The 2024 CCPC Online Contest

Luckyblock發表於2024-09-09

目錄
  • 寫在前面
  • L
  • K
  • B
  • D
  • E
  • J
  • I
  • G
  • C
  • 寫在最後

寫在前面

補題地址:https://codeforces.com/gym/105336

以下按個人向難度排序。

唉唉唐吧真是

我去居然還能打出名額哈哈真牛逼

L

簽到。

K

簽到。

dztlb 大神直接秒了。

顯然奇數先手取 1 必勝,則偶數時為了讓自己不輸,先手和後手均會保證在取到 2 之前,取的數和沙堆的大小一直都是偶數,否則會讓對方必勝。

於是一個顯然的想法是考慮 \(\operatorname{lowbit}(n)\),發現當 \(\operatorname{lowbit}(n)\le k\) 時,僅需先手取 \(\operatorname{lowbit}(n)\) 然後模仿後手操作即可,否則一次操作後 \(\operatorname{lowbit}(n)\le k\),則後手必勝。

#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define ull unsigned long long

int T,n,k;
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin>>T;
	while(T--){
		bool fl=0;
		cin>>n>>k;
		int tt=1;
		while(tt<=k){
			if(n%tt==0){
				if((n/tt)%2==1) fl=1;
			}
			tt*=2;	
		}
		if(fl) puts("Alice");
		else puts("Bob");
	}
	return 0;
}

B

列舉,結論。

一個顯然的發現是有序(升序或降序)數列代價最小,可以考慮調整法證明,當某兩位置不有序時,一定會對包含這兩個位置的一些區間造成更多的代價。於是僅需考慮重複元素之間的順序的貢獻即可。

注意特判全相等時升序降序是等價的。

#include <bits/stdc++.h>
using namespace std;

#define LL long long
#define ull unsigned long long

const int kN = 1e6 + 10;
const LL p = 998244353;
int n, a[kN], cnt[kN], num;
LL fac[kN];

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	
	std::cin >> n;
	for (int i = 1; i <= n; ++ i) std::cin >> a[i];
	fac[1] = 1;
	for (int i = 2; i <= n; ++ i) fac[i] = fac[i - 1] * i % p;
	std::sort(a + 1, a + n + 1);
	
	LL ans1 = 0;
	for (int l = 1; l <= n; ++ l) {
		for (int r = l; r <= n; ++ r) {
			ans1 += 1ll * (a[r] - a[l]);
		}
	}
	
	for (int i = 1; i <= n; ++ i) {
		++ cnt[a[i]];
		if (cnt[a[i]] == 1) ++ num;	
	}
	LL ans2 = (num == 1 ? 1 : 2);
	for (int i = 1; i <= n; ++ i) {
		if (cnt[a[i]] == 0) continue;
		ans2 = ans2 * fac[cnt[a[i]]] % p;
		cnt[a[i]] = 0;
	}
	std::cout << ans1 << " " << ans2 % p;
	return 0;
}

D

DP。

顯然的 DP,發現構造字串的方案實際構成一個完全二叉樹的結構,於是僅需考慮在構造這棵樹的過程中,統計完全位於兩棵子樹中的字串數量,以及跨越根節點的字串數量即可。

於是可以設一個顯然的狀態 \(f_{i, l, r}\) 表示對於字首 \(1~i\) 中,子串 \(t[l:r]\) 的出現次數,轉移時討論跨越子樹的字串中,根節點是否有貢獻即可。詳見程式碼。

總時間複雜度 \(O(n^4)\) 級別。

#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define ull unsigned long long

const int kN = 110;
const ll p = 998244353;

ll f[kN][kN][kN], pw2[kN];
int n, m;
string s, t;

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	std::cin >> s >> t;
	n = s.length(), m = t.length();
	s = "$" + s, t = "$" + t;
	
	
	pw2[0] = 1; 
	for (int i = 1; i <= 10; ++ i) pw2[i] = pw2[i - 1] * 2;
	
	for (int i = 1; i <= n; ++ i) {
		for (int l = 1; l <= m + 1; ++ l) {
			for (int r = 0; r < l; ++ r) {
				f[i - 1][l][r] = 1;
			}
		}
		
		for (int l = 1; l <= m; ++ l) {
			for (int r = l; r <= m; ++ r) {
				int len = r - l + 1;
				if (i <= 8 && pw2[i] < len) break;
				
				f[i][l][r] = 2ll * f[i - 1][l][r] % p;
				for (int j = l; j < r; ++ j) {
					f[i][l][r] += 1ll * f[i - 1][l][j] * f[i - 1][j + 1][r] % p;
					f[i][l][r] %= p;
				}
				for (int j = l; j <= r; ++ j) {
					if (s[i] != t[j]) continue;
					f[i][l][r] += 1ll * f[i - 1][l][j - 1] * f[i - 1][j + 1][r] % p;
					f[i][l][r] %= p;
				}
			}
		}
	}
	cout << f[n][1][m];
	return 0;
}
/*
aaaaaa a

abc abca
*/

E

期望。

呃呃這題場上三個人看了 4h 中間過了三道題沒做出來真唐吧

最大數量即儘量使所有字串沒有公共字首,即儘可能使每一層的節點均填滿。則答案即為:

\[\sum_{i=0}^{m} \min(n, 26^i) \]

考慮期望。插入的每個字串長度均為 \(m\),則每個字串都會佔據 \(1\sim m\) 層的一個節點。即對於建出來的字典樹,第 \(i\) 層上的所有節點,一定是對應了這 \(n\) 個字串的長度為 \(i\) 的字首。又字串隨機構造,則相當於在第 \(i\) 層上隨機選擇了 \(n\) 個節點,並求隨機選擇節點的數量。

考慮每一個節點對這一層的貢獻再乘上本層總數即為本層貢獻。考慮反面,求該節點沒有被選擇的機率(相當於限制僅能從 \(26^i - 1\) 個節點中獨立地隨便選 \(n\) 個),則可知期望的總數為:

\[\sum_{i=0}^{m} \left[ 1 - \left(\frac{26^i - 1}{26^i}\right)^n \right] 26^i \]

#include <bits/stdc++.h>
using namespace std;

#define LL long long
#define ull unsigned long long

const int kN = 1e6 + 10;
const LL p = 998244353;

LL n, m;

LL qpow(LL x_, LL y_) {
	LL ret = 1;
	while (y_) {
		if (y_ & 1ll) ret = ret * x_ % p;
		x_ = x_ * x_ % p, y_ >>= 1ll;
	}
	return ret;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	std::cin >> n >> m;
	
	LL ans = 0;
	for (int i = 0; i <= m; ++ i) {
		if (i <= 3 && qpow(26, i) <= n) ans += qpow(26, i);
		else ans += n;
	}
	cout << ans % p << " ";
	
	LL sum = 0, inv = qpow(26, p - 2), invpw = 1, pw = 1;
	for (int i = 0; i <= m; ++ i) {
		LL s = (1 - qpow(1 - invpw + p, n) + p) % p * pw % p;
		invpw = invpw * inv % p, pw = pw * 26 % p;
		sum += s, sum %= p;
	}
	cout << sum;
	return 0;
}

J

異或,線性基

感覺線性基裸題啊呃呃

考慮令 \(c_i = a_i\oplus b_i\),並預處理 \(s_a = \oplus_{1\le i\le n} a_i, s_b = \oplus_{1\le i\le n} b_i\)。由異或的性質,則交換 \(a_i, b_i\)\(s_a, s_b\) 的影響等價於令兩者均異或上 \(c_i\)。問題變為可以任意異或 \(c_i\),求 \(\min \max(s_a, s_b)\)

考慮從高位到低按位貪心,設當前列舉到第 \(i\) 位,且有 \(\max(s_a, s_b) = s_{1}\),另一方為 \(s_{2}\)

  • \(s_1, s_2\) 該位均為 0,或僅有 \(s_2\) 該位為 0,則異或後不會使答案更優,跳過;
  • \(s_1\) 該位為 1,則考慮儘量將這一位消除,且不影響已確定的更高的位的答案。

由上述不影響更高位的要求,想到考慮對 \(c_i\) 維護線性基,然後直接在上述貪心過程中使用對應的向量消去答案中某位的 1 即可。

#include <bits/stdc++.h>
using namespace std;

#define int long long
#define ull unsigned long long

const int kN = 1e6 + 10;

int n, a[kN], b[kN], suma, sumb, c[kN];
int ans, p[70];

void insert(int c_) {
	for (int i = 60; ~i; -- i) {
		if (!(c_ >> i)) continue;
		if (!p[i]) {
			p[i] = c_;
			break;
		}
		c_ ^= p[i];
	}
}
void solve(int sa_, int sb_) {
	int x = sa_, y = sb_;
	for (int i = 60; ~i; -- i) {
		if (x < y) std::swap(x, y);
		if (!(x >> i & 1) && !(y >> i & 1)) continue;
		if ((x >> i & 1) && (y >> i & 1)) {
			x ^= p[i], y ^= p[i];
		} else if ((x >> i & 1) && x >= y) {
			x ^= p[i], y ^= p[i];
		} else {
			continue;
		}
	}
	ans = min(ans, max(x, y));
}

signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int T; std::cin >> T;
	while (T --) {
		std::cin >> n;
		suma = sumb = 0;
		for (int i = 1; i <= n; ++ i) std::cin >> a[i], suma ^= a[i];
		for (int i = 1; i <= n; ++ i) std::cin >> b[i], sumb ^= b[i];
		
		for (int i = 0; i <= 60; ++ i) p[i] = 0;
		for (int i = 1; i <= n; ++ i) c[i] = a[i] ^ b[i], insert(c[i]);
		
		ans = max(suma, sumb);
		solve(suma, sumb);
		cout << ans << "\n";
	}
	return 0;
}
/*
1
2
2147483646 2147483647
1 2147483647
*/

I

DP。

dztlb 大神寫的,看起來是簡單題,就不補了。

code by dztlb:

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define ull unsigned long long
//
//int read()
//{
//	int x = 0; bool f = false; char c = getchar();
//	while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
//	while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
//	return f ? -x : x;
//}
const int N=505;
const int mod=998244353;
int n,m;
int a[N],b[N];
int sum[N];
int f[N][N];
int dp[N];
void solve(int k){
	memset(f,0,sizeof(f));
	sum[0]=0;
	for(int i=0;i<=m;++i) f[i][0]=1;
	for(int i=1;i<=m;++i){
		for(int j=1;j<=min(i, n);++j){
			if(j>sum[max(b[i]-k,0ll)]) break;
			f[i][j]=f[i-1][j]+f[i-1][j-1]*max(sum[max(b[i]-k,0ll)]-j+1,0ll)%mod;
			f[i][j]%=mod;
		}
	}
	for(int i=1;i<=min(n, m);++i){
		dp[k]+=f[m][i];
		dp[k]%=mod;
	}
}
signed main() {
//	ios::sync_with_stdio(false);
//	cin.tie(0), cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) {
		cin>>a[i];
		sum[a[i]]++;
	}
	
	for(int i=1;i<=m;++i) cin>>b[i];
	sort(b+1,b+1+m);
	sort(a+1,a+1+n);
	int maxx=0;
	int minn=1000;
	for(int i=1;i<=m;++i){
		for(int j=b[i]-1;j>=1;--j){
			if(sum[j]) minn=min(minn,b[i]-j),maxx=max(maxx,b[i]-j);
		}
	}
	if(minn==1000||maxx==0){
		puts("0");
		return 0;
	}
	for(int i=1;i<=500;++i) sum[i]+=sum[i-1];
//	minn=1;
//	cout<<minn<<' '<<maxx<<endl;
	for(int k=minn;k<=maxx;++k){
		solve(k);
	}
	int ans=dp[maxx]*maxx%mod;
	for(int i=minn;i<maxx;i++){
		ans+=(dp[i]-dp[i+1]+mod)%mod*(i)%mod;
		ans%=mod;
	}
	cout<<ans<<endl;
	return 0;
}
/*
5 5
1 2 3 4 5
2 3 4 5 6
*/

G

網路流

顯然應當讓第一個人儘量在他要吃的所有菜都買單,於是可以直接計算出第一個人花的錢的上界,其他人花的錢的上界一定是第一個人的上界減 1,問能否在所有上界滿足情況下能買所有菜。

僅有上界則是顯然的最大流問題,建圖求最大流檢查是否等於所有菜的總價即可。

注意建邊時的實際意義一定要符合真實情況!

//知識點:
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const int kM = 2e6 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, m, S, T, a[kN], car[kN];
int x[kN], y[kN], dish[kN];
int nodenum, maxnodenum, edgenum = 1, v[kM], ne[kM], head[kN];
int cur[kN], dep[kN];
LL w[kM];
LL flag, maxa, maxflow;
//=============================================================
void addedge(int u_, int v_, LL w_) {
  v[++ edgenum] = v_;
  w[edgenum] = w_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;

  v[++ edgenum] = u_;
  w[edgenum] = 0;
  ne[edgenum] = head[v_];
  head[v_] = edgenum;
}
void init() {
  std::cin >> n >> m;
  
  edgenum = 1;
  nodenum = n + m;
  maxnodenum = n + m + 2;
  S = ++ nodenum, T = ++ nodenum;
  for (int i = 1; i <= maxnodenum; ++ i) head[i] = 0;

  for (int i = 1; i <= n; ++ i) {
    std::cin >> a[i] >> car[i]; 
  }
  maxa += car[1];
  for (int i = 1; i <= m; ++ i) {
    std::cin >> x[i] >> y[i] >> dish[i];
    if (x[i] == 1 || y[i] == 1) maxa = std::min(1ll * a[1], maxa + dish[i]);
  }

  addedge(S, 1, maxa - car[1]);
  for (int i = 2; i <= n; ++ i) {
    if (car[i] >= maxa) flag = 1;

    LL w_ = std::max(0ll, maxa - car[i] - 1);
    w_ = std::min(std::max(0ll, 1ll * a[i] - car[i]), w_);
    addedge(S, i, w_);
  }
  for (int i = 1; i <= m; ++ i) {
    addedge(x[i], i + n, kInf);
    addedge(y[i], i + n, kInf);
    addedge(i + n, T, dish[i]);
    maxflow += dish[i];
  }
}
bool bfs() {
  std::queue <int> q;
  memset(dep, 0, (nodenum + 1) * sizeof (int));
  dep[S] = 1; //注意初始化 
  q.push(S); 
  while (!q.empty()) {
    int u_ = q.front(); q.pop();
    for (int i = head[u_]; i > 1; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (w_ > 0 && !dep[v_]) {
        dep[v_] = dep[u_] + 1;
        q.push(v_);
      }
    }
  }
  return dep[T];
}
LL dfs1(int u_, LL into_) {
  if (u_ == T) return into_; 
  int ret = 0;
	for (int i = cur[u_]; i > 1 && into_; i = ne[i]) {
    int v_ = v[i];
    LL w_ = w[i];
    if (w_ && dep[v_] == dep[u_] + 1) {
			LL dist = dfs1(v_, std::min(into_, w_));
			if (!dist) dep[v_] = kN;
			into_ -= dist; 
      ret += dist;
      w[i] -= dist, w[i ^ 1] += dist;
			if (!into_) return ret;
		}
  }
	if (!ret) dep[u_] = 0; 
  return ret;
}
LL dinic() {
  LL ret = 0;
  while (bfs()) {
    memcpy(cur, head, (nodenum + 1) * sizeof (int));
    ret += dfs1(S, kInf);
  }
  return ret;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  init();
  if (flag) {
    std::cout << "NO\n";
    return 0;
  }
  std::cout << ((dinic() == maxflow) ? "YES" : "NO") << "\n";
  return 0;
}
/*
4 3
1000 900
1000 900
1000 900
1000 900
1 2 100
1 3 100
1 4 100

3 1
100 50
70 50
100 48
3 3 1
*/

C

貪心,DP。

場上想出來那個樹形 DP 但是覺得討論起來會很麻煩就沒寫。

寫在最後

學到了什麼:

  • E:每個元素均等機率情況,考慮單個元素出現在答案中的期望機率,然後乘上元素數量即為總貢獻。
  • G:注意實際意義!

相關文章