2024牛客暑期多校訓練營4

Luckyblock發表於2024-07-26

目錄
  • 寫在前面
  • 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}\) 個,答案即為:

\[\left\lceil\dfrac{\sum\limits_{i\bmod 3 = 2} \operatorname{cnt}_{i} }{2}\right\rceil + \sum_{i} \left\lfloor\dfrac{\operatorname{cnt}_i}{3}\right\rfloor \]

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\),則有顯然的轉移:

\[f_{i, \operatorname{len}} = \begin{cases} 0 &s_{i} = 0\\ f_{i - 1, \operatorname{len} - 1} &s_{i} = 1\\ \frac{f_{i - 1, \operatorname{len} - 1}}{2} &s_{i} = \text{?} \end{cases}\]

考慮記 \(g_{i, j}\) 表示以 \(i\) 為右端點的區間在給定 \(k=j\) 情況下對答案的貢獻之和,考慮列舉區間長度 \(\operatorname{len}\),即有:

\[g_{i, j} = \sum_{\operatorname{len} = 1}^{i} f_{i, \operatorname{len}}\times \operatorname{len}^j \]

最終答案即為:

\[\sum_{1\le i \le n} g_{i, k} \]

發現這個狀態和轉移都是 \(O(n^2)\) 的,看著就非常不可直接做的樣子呃呃,但考慮將 \(f\) 的轉移代入嘗試將兩個狀態合併,以 \(s_i=1\)\(f_{i, \operatorname{len}} = f_{i - 1, \operatorname{len} - 1}\) 為例,有:

\[\begin{aligned} g_{i, j}&=\sum_{\operatorname{len} = 1}^{i} f_{i, \operatorname{len}}\times \operatorname{len}^j = \sum_{\operatorname{len} = 1}^{i} f_{i - 1, \operatorname{len} - 1}\times \operatorname{len}^j \end{aligned}\]

感覺這裡面可以摳出一個 \(g_{i - 1, j}\),考慮到 \(\operatorname{len}=0\) 時實際無貢獻,再考慮二項式定理展開嘗試一下:

\[\begin{aligned} g_{i, j}&= \sum_{\operatorname{len} = 1}^{i} f_{i - 1, \operatorname{len} - 1}\times \operatorname{len}^j\\ &=\sum_{\operatorname{len} = 1}^{i-1} f_{i - 1, \operatorname{len}} \times(\operatorname{len}+1)^j\\ &=\sum_{\operatorname{len} = 1}^{i-1} f_{i - 1, \operatorname{len}} \sum_{l=0}^{j}{j\choose l}\operatorname{len}^{l}\\ &=\left(\sum_{\operatorname{len} = 1}^{i-1} f_{i - 1, \operatorname{len}}\times \operatorname{len}^{j}\right) + \left(\sum_{\operatorname{len} = 1}^{i-1} f_{i - 1, \operatorname{len}} \sum_{l=0}^{j-1}{j\choose l}\operatorname{len}^{l}\right)\\ &=g_{i - 1, j} + \left(\sum_{\operatorname{len} = 1}^{i-1} f_{i - 1, \operatorname{len}} \sum_{l=0}^{j-1}{j\choose l}\operatorname{len}^{l}\right) \end{aligned}\]

後面這一坨什麼玩意兒?再拿出來推一下:

\[\begin{aligned} \sum_{\operatorname{len} = 1}^{i-1} f_{i - 1, \operatorname{len}} \sum_{l=0}^{j-1}{j\choose l}\operatorname{len}^{l} &= \sum_{l=0}^{j - 1}{j\choose l}\sum_{\operatorname{len}=1}^{i - 1}f_{i - 1, \operatorname{len}}\times \operatorname{len}^{l}\\ &= \sum_{l=0}^{j - 1}{j\choose l}g_{i-1,l} \end{aligned}\]

整理一下,則當 \(s_{i} = 1\) 時有:

\[g_{i, j} = g_{i - 1,j} + \sum_{l=0}^{j - 1}{j\choose l}g_{i-1,l} \]

\(s_{i} = \text{?}\) 時同理,僅需乘個係數 \(\frac{1}{2}\) 即可:

\[g_{i, j} = \dfrac{1}{2}\left(g_{i - 1,j} + \sum_{l=0}^{j - 1}{j\choose l}g_{i-1,l}\right) \]

上述轉移已經可以直接做了。預處理下組合數,轉移時列舉位置 \(i\) 與次數 \(j\) 大力更新即可,總時間複雜度為 \(O(nk^2)\) 級別,可以透過本題。


寫在最後

學到了什麼:

  • A:注意特殊條件!!!
  • H:觀察操作的性質,並做等價轉化。

相關文章