- 寫在前面
- G
- C
- I
- A
- H
- F
- J
- 寫在最後
寫在前面
比賽地址:https://ac.nowcoder.com/acm/contest/81599
以下按個人向難度排序。
媽的這場簽到相當順前五個題都沒怎麼卡,被 F 搞崩了媽的,型號後面還是過了,J 到最後也沒想到怎麼把 \(k\) 扔到複雜度裡太幾把了呃呃
G
簽到。
code by dztlb:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int t;
double xg,yg,xt,yt;
double dis(double a,double b,double c,double d){
return sqrt((a-c)*(a-c)+(b-d)*(b-d));
}
signed main(){
cin>>t;
while(t--){
cin>>xg>>yg>>xt>>yt;
double ans=min(dis(xg,-1.00*yg,xt,yt),dis(-1.00*xg,yg,xt,yt));
printf("%.10lf\n",ans);
}
return 0;
}
/*
1
5 4 15
1 1 B
ABAB
BABA
ABAB
BABA
ABAB
*/
C
簽到。
考慮最優的操作,顯然放到正確位置上的數一定不會再被操作,則想到應當每次選擇權值 \(i, a_{i}, a_{a_i}, a_{a_{a_i}}\),並將 \(i, a_i, a_{a_i}\) 三個權值放到正確的位置上。這樣操作可使每次操作均將最多的數放到正確位置上,若不這樣操作,則之後還要透過操作將它們放到正確位置上,則這樣操作一定不會更劣。
太套路了,套路地轉換成圖論模型,節點 \(i\) 向節點 \(a_{i}\) 連邊,由於排列的性質顯然構成了若干環。則上述操作等價於在環上選擇三個相連的點刪掉,若刪沒了或刪的只剩 1 個則說明經過上述操作即可將所有數歸位,若剩下 2 個則還需要操作,但發現一次操作可以同時處理兩個大小為 2 的環,於是需要特判一下。
記大小為 \(i\) 的環有 \(\operatorname{cnt}\) 個,答案即為:
code by dztlb:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int t;
int n;
int a[N],siz,st;
bool vis[N];
int c[5],top;
void dfs(int x){
vis[x]=1;
++siz;
if(x==st) return;
dfs(a[x]);
}
signed main(){
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i];
vis[i]=0;
}
memset(c,0,sizeof(c));
int ans=0;
for(int i=1;i<=n;++i){
if(a[i]==i||vis[i]) continue;
siz=0;
st=i;
dfs(a[i]);
ans+=siz/3;
c[siz%3]++;
// cout<<siz<<endl;
}
ans+=c[2]/2;
if(c[2]%2==1) ans++;
cout<<ans<<'\n';
}
return 0;
}
/*
1
10
1 2 9 10 3 6 8 4 5 7
*/
I
列舉。
發現完全圖的所有子圖都是完全圖,一個顯然的想法是列舉題目要求的完全圖的左右端點 \([l, r]\),則對於以 \(l\) 為左端點的極大的構成完全圖的區間 \([l, r]\),當 \(l\) 遞增時 \(r\) 一定不減。
於是考慮雙指標列舉區間,發現僅需檢查每次右端點 \(r+1\) 時,端點 \(r+1\) 與當前區間 \([l, r]\) 的點是否都有連邊即可。可以對於每個端點 \(i\) 都維護有哪些編號小於 \(i\) 的點與它有連邊,則對 \(r+1\) 二分檢查不小於 \(l\) 的數是否是 \(r-l\) 個即可。
總時間複雜 \(O(n)\) 級別。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n, m, du[kN];
std::vector<int> edge[2][kN];
bool yes;
LL ans;
//=============================================================
bool check(int L_, int pos_) {
if (edge[0][pos_].empty()) return false;
int p = std::lower_bound(edge[0][pos_].begin(), edge[0][pos_].end(), L_) - edge[0][pos_].begin();
if (edge[0][pos_][p] != L_) return false;
int sz = edge[0][pos_].size() - p;
return (sz == (pos_ - L_));
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) du[i] = 1;
for (int i = 1; i <= m; ++ i) {
int u, v; std::cin >> u >> v;
edge[0][v].push_back(u);
edge[1][u].push_back(v);
}
for (int i = 1; i <= n; ++ i) {
std::sort(edge[0][i].begin(), edge[0][i].end());
std::sort(edge[1][i].begin(), edge[1][i].end());
}
yes = 1;
int l = 1, r = 1;
for (; l <= n; ++ l) {
r = std::max(r, l);
while (r + 1 <= n && check(l, r + 1)) ++ r;
ans += 1ll * r - l + 1;
// del(l);
}
std::cout << ans << "\n";
return 0;
}
A
樹,並查集。
欽定了每次詢問的點都是樹的根節點,一個很顯然的想法是直接對於每棵樹維護根的答案 \(\operatorname{ans}_{\operatorname{root}}\),考慮新連一條邊的影響。
發現每次連邊均為將一個根 \(u_1\) 接到另外一棵根為 \(u_2\) 的樹的的節點 \(v\) 上,則根 \(u_2\) 的答案要麼不變,要麼更新為 \(\operatorname{dis}(u_2, v) + \operatorname{ans}_{u_1}\)。
\(\operatorname{dis}\) 即原樹上兩點深度差,維護根使用並查集即可。總時間複雜度 \(O(n)\) 級別。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n, ans[kN], fa[kN], directfa[kN], dep[kN];
int a[kN], b[kN], c[kN];
//=============================================================
int find(int x_) {
return (fa[x_] == x_) ? (x_) : (fa[x_] = find(fa[x_]));
}
void merge(int x_, int y_) {
int fx = find(x_), fy = find(y_);
fa[fx] = fy;
}
void dfs(int u_) {
if (!directfa[u_]) {
dep[u_] = 1;
return ;
}
if (dep[u_]) return ;
dfs(directfa[u_]);
dep[u_] = dep[directfa[u_]] + 1;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n;
for (int i = 1; i <= n; ++ i) dep[i] = directfa[i] = 0;
for (int i = 1; i < n; ++ i) {
std::cin >> a[i] >> b[i] >> c[i];
directfa[b[i]] = a[i];
}
for (int i = 1; i <= n; ++ i) if (!dep[i]) dfs(i);
for (int i = 1; i <= n; ++ i) ans[i] = 1, directfa[i] = 0, fa[i] = i;
for (int i = 1; i < n; ++ i) {
merge(b[i], a[i]);
directfa[b[i]] = a[i];
int top = find(a[i]), u = a[i], delta = ans[b[i]];
ans[top] = std::max(ans[top], dep[u] - dep[top] + 1 + delta);
std::cout << ans[c[i]] - 1 << " ";
}
std::cout << "\n";
}
return 0;
}
H
思維,數學,輾轉相減法。
首先重複權值是沒用的,先去個重,排個序,特判下 \(n=1\) 時答案為 0。
發現一種非常好的操作方案是每次選擇次大值或次小值作為 \(a_{p}\),然後僅對最大值/最小值進行操作從而減少整個數列的極差。
發現當 \(n=3\) 時,這個過程相當於不斷地對兩種差值做輾轉相減;擴充套件到 \(n\) 更大,發現這相當於被操作的最大值/最小值之外的所有部分,與最大值/最小值的差值做輾轉相減,則答案實際上即輾轉相減的結果——排序後整個數列相鄰之差的 \(\operatorname{gcd}\)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int t;
int n,ans;
int a[N];
signed main(){
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i];
}
sort(a+1,a+1+n);
if(n==1){
puts("0"); continue;
}
ans=a[2]-a[1];
for(int i=2;i<n;++i){
int t=a[i+1]-a[i];
ans=__gcd(ans,t);
}
cout<<ans<<'\n';
}
return 0;
}
/*
2
3
1 3 100
4
1 1 1 1
*/
F
構造。
場上構造的太簡單了呃呃就只證出來個上界,後面試了幾發猜了個暴論特判過了呃呃呃
發現使用 \(n\) 個點構造出的 \(x\) 的上界在鏈時取到,但界內可能有些 \(x\) 無法構造得到,但是發現只要再加 1 個點一定可以。手玩了 \(n=7,8,9\) 的例子發現需要保證取到上界的 \(n\) 若與 \(x\) 奇偶性不同則無法構造得到需要加 1,若奇偶性相同則可以於是特判過了呃呃
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e7+5;
int t,n,x;
int getans(int n){
if(n%2==0){
return n*(n-1)/2-(1+(n-2)/2)*(n-2)/2-n/2;
}else return n*(n-1)/2-(1+(n-1)/2)*(n-1)/2;
}
signed main(){
cin>>t;
while(t--){
cin>>x;
if(x<=3){
cout<<x+2<<'\n'; continue;
}
int l=5,r=2100000000ll,ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(getans(mid)>=x){
ans=mid;
r=mid-1;
}else l=mid+1;
}
if(getans(ans)%2==x%2)cout<<ans<<'\n';
else if(ans%2==0&&x%2==1) cout<<ans+1<<'\n';
else cout<<ans<<'\n';
}
return 0;
}
/*
2
3
1 3 100
4
1 1 1 1
*/
J
列舉,數學
場上想出了一萬個 \(O(n^2)\) 做法呃呃呃呃太傻比了、、、不過要是敢交一發就過了也是夠搞、、、
以下我的推法和題解本質相同但是列舉順序不太一樣,注意不要搞混了、、、
一個顯然的想法是考慮每種長度的全 1 區間的期望數量有多少。發現在計算期望數量時需要考慮區間內有多少 ?
以考慮其出現機率,於是考慮在列舉區間右端點的同時維護下區間內有多少 ?
,可以想到一個顯然的二維 DP 狀態 \(f_{i, \operatorname{len}}\) 表示區間右端點為 \(i\),區間長度為 \(j\) 的區間的期望數量,初始化 \(f_{i, 0} = 1\),則有顯然的轉移:
考慮記 \(g_{i, j}\) 表示以 \(i\) 為右端點的區間在給定 \(k=j\) 情況下對答案的貢獻之和,考慮列舉區間長度 \(\operatorname{len}\),即有:
則最終答案即為:
發現這個狀態和轉移都是 \(O(n^2)\) 的,看著就非常不可直接做的樣子呃呃,但考慮將 \(f\) 的轉移代入嘗試將兩個狀態合併,以 \(s_i=1\) 時 \(f_{i, \operatorname{len}} = f_{i - 1, \operatorname{len} - 1}\) 為例,有:
感覺這裡面可以摳出一個 \(g_{i - 1, j}\),考慮到 \(\operatorname{len}=0\) 時實際無貢獻,再考慮二項式定理展開嘗試一下:
後面這一坨什麼玩意兒?再拿出來推一下:
整理一下,則當 \(s_{i} = 1\) 時有:
\(s_{i} = \text{?}\) 時同理,僅需乘個係數 \(\frac{1}{2}\) 即可:
上述轉移已經可以直接做了。預處理下組合數,轉移時列舉位置 \(i\) 與次數 \(j\) 大力更新即可,總時間複雜度為 \(O(nk^2)\) 級別,可以透過本題。
寫在最後
學到了什麼:
- A:注意特殊條件!!!
- H:觀察操作的性質,並做等價轉化。