普通高中生的异界双重契约~诅咒伪后宫与右眼神格觉醒!第二章!

1. NOI2019 - 序列 \(\color{red}\texttt{战败}\)

https://qoj.ac/contest/542/problem/1225
题意:给定长度为 \(n\) 的序列 \(a,b\),要求选出两个集合 \(S,T\) 满足 \(|S|=|T|=k,|S\cap T| \geq l\),求 \(\sum_{i\in S} a_i+\sum_{i\in T} b_i\) 的最大值。
\(n\leq 2e5,1\leq a,b\leq 1e9\)

交集 \(\geq l\),意味着对称差 \(\leq k-l\)。而这个东西是可以用费用流描述的。建立 \(S,T\) 和点 \([1],[2],...,[n],(1),(2),...,(n)\) 以及点 \(U,V\)。连边:

  • \((S,[i],1,a_i)\) 表示选择 \(a_i\)
  • \(((i),T,1,b_i)\) 表示选择 \(b_i\)
  • \(([i],(i),1,0)\) 表示选择 \(a_i,b_i\),不会有任何限制。
  • \(([i],U,1,0),(U,V,k-l,0),(V,(j),1,0)\),表示选择 \(a_i,b_j\),限制流量 \(\leq k-l\)

直接跑流 \(k\) 条的最大费用流即可获得 \(64\) 分。

考虑模拟费用流。实际上有五种增广路方式:

  • \([i]\to (i)\to V\to U\to [j]\to (j)\),表示将 \((a_j,b_i)\) 增加为 \((a_i,b_i),(a_j,b_j)\),对 \(U\to V\) 边流量贡献为 \(-1\)
  • \([i]\to (i)\),表示选择 \((a_i,b_i)\)
  • \([i]\to U\to[j]\to(j)\),表示将 \((a_j,b_k)\) 增加为 \((a_i,b_k),(a_j,b_j)\)
  • \([i]\to (i)\to V\to (j)\),与上一种类似。
  • \([i]\to U\to V\to (j)\),表示选择 \((a_i,b_j)\),对 \(U\to V\) 边流量贡献为 \(+1\)

可以证明如果增广路经过很多下标,一定有一种属于上述五种的增广路不劣。

那么模拟 EK 的过程如何呢?分类考虑五种情况要求是什么。

  • 第一种,要求 \(i\) 的右侧有流量,左侧没有流量;\(j\) 的左侧有流量,右侧没有流量。
  • 第二种,要求 \(i\) 两侧都没有流量。
  • 第三种,要求 \(i\) 左侧没有流量;\(j\) 左侧有流量,右侧没有流量。
  • 第四种与上一种类似。
  • 第五种,要求 \(i\) 左侧没有流量;\(j\) 右侧没有流量。

左侧没有流量 与 右侧有流量,左侧没有流量 两个的区别在图上表示出来就是与 \(U,V\) 的连边的方向。

容易发现上述有五种形式,使用五个堆分别维护上述五种最大值和下标,然后取最大的一种增广即可。

注意:上述五种转移对于 \(U\to V\) 流量贡献是 \(1\leq 2,3,4\leq 5\),所以相同费用应先流 \(1\) 最后流 \(5\)

维护细节:将所有可行的插入堆中,每次更新弹出堆顶不合法状态,然后枚举五种,选择一种更新。检查不合法方式是维护 \(va,vb\) 表示 \(i\) 两端是否有流量。

复杂度 \(O(n\log n)\)

点击查看代码
//qoj1225
#include <bits/stdc++.h>
using namespace std;
#define sc second
#define fi first
#define mp make_pair
const int N = 2e5 + 10;
const int inf = 1e9 + 10;
typedef pair<int, int> pii;
int T, n, k, l, a[N], b[N];
bool va[N], vb[N];
priority_queue<pii> q, qa, qb, qaa, qbb;
void aa(int x){ va[x] = 1; if(!vb[x]) qbb.push(mp(b[x], x)); }
void bb(int x){ vb[x] = 1; if(!va[x]) qaa.push(mp(a[x], x)); }

int main(){
  scanf("%d", &T);
  while(T--){
    scanf("%d%d%d", &n, &k, &l);
    l = k - l;
    for(int i = 1; i <= n; ++ i) scanf("%d", &a[i]);
    for(int i = 1; i <= n; ++ i) scanf("%d", &b[i]);
    while(!q.empty()) q.pop(); q.push(mp(-inf, 0));
    while(!qa.empty()) qa.pop(); qa.push(mp(-inf, 0));
    while(!qb.empty()) qb.pop(); qb.push(mp(-inf, 0));
    while(!qaa.empty()) qaa.pop(); qaa.push(mp(-inf, 0));
    while(!qbb.empty()) qbb.pop(); qbb.push(mp(-inf, 0));
    for(int i = 1; i <= n; ++ i){
      va[i] = vb[i] = 0;
      q.push(mp(a[i]+b[i], i));
      qa.push(mp(a[i], i));
      qb.push(mp(b[i], i));
    }
    long long ans = 0;
    for(int i = 1; i <= k; ++ i){
      while(va[q.top().sc] || vb[q.top().sc]) q.pop(); pii nw = q.top();
      while(va[qa.top().sc]) qa.pop(); pii nwa = qa.top();
      while(vb[qb.top().sc]) qb.pop(); pii nwb = qb.top();
      while(va[qaa.top().sc]) qaa.pop(); pii nwaa = qaa.top();
      while(vb[qbb.top().sc]) qbb.pop(); pii nwbb = qbb.top();
      int op = 2, mx = nw.fi;
      if(mx < nwaa.fi + nwbb.fi) mx = nwaa.fi + nwbb.fi, op = 1;
      if(mx < nwa.fi + nwbb.fi) mx = nwa.fi + nwbb.fi, op = 3;
      if(mx < nwaa.fi + nwb.fi) mx = nwaa.fi + nwb.fi, op = 4;
      if(mx < nwa.fi + nwb.fi && l && nwa.sc != nwb.sc) mx = nwa.fi + nwb.fi, op = 5;
      if(op == 1) ++ l, aa(nwaa.sc), bb(nwbb.sc);
      if(op == 2) aa(nw.sc), bb(nw.sc);
      if(op == 3) aa(nwa.sc), bb(nwbb.sc);
      if(op == 4) aa(nwaa.sc), bb(nwb.sc);
      if(op == 5) -- l, aa(nwa.sc), bb(nwb.sc);
      ans += mx;
    }
    printf("%lld\n", ans);
  }
  return 0;
}

2. qoj9438 - Two Box \(\color{red}\texttt{战败}\)

https://qoj.ac/contest/1804/problem/9438
题意:有 \(1\sim m\) 每个编号的石子各一个,两个箱子,一个白一个黑。刚开始所有石子在白箱子里。接下来 \(n\) 次操作,第 \(i\) 次操作自选一个石子移到另一个箱子中,要求是操作结束后黑箱子里所有石子编号在 \([1,a_i]\) 之中。\(q\) 次单点改 \(a\),询问合法操作序列个数。
\(n,q\leq 3e4,m\leq 15\)

单组询问怎么做都行,考虑如何进行多组询问。

画一个直方图,那么可以观察到的是对于所有一行内的那些极长区间,构成一个树形结构。对于一个第 \(i\) 层的区间,出了这个区间就和他 \([i+1,m]\) 部分的操作没有关系了,因为这部分肯定都在白箱子里。

那么定义一个 \(f_{i,j,S}\) 表示 \(i\) 层第 \(j\) 个区间,\([1,i]\) 内石子选择状态改变了 \(S\) 的方案数。那么从 \(f_{i+1,[l,r]}\) 转移到 \(f_{i,j}\) 就是提取出所有 \(S\)\(i+1\) 位为 \(0\) 的状态,然后异或卷积。

注意到,实际上对于 \(\operatorname{popcount}(S)\) 相同的 \(S\),他们的值也应该相同,因为那些石子目前可以看做是相同的。所以可以在 \(O(m^2)\) 时间内进行 FWT 卷积。

对于修改,显然只会改 \(O(m)\) 个区间,可以使用线段树维护。

复杂度 \(O((n+q)m^2(m+\log n))\)

3. CF1456E - XOR-ranges \(\color{blue}\texttt{讲课}\)

https://www.luogu.com.cn/problem/CF1456E。
题意:构造一个长度为 \(n\) 序列 \(a\) 满足 \(a_i\in[l_i,r_i]\),且代价最小。代价定义为对于任意一个二进制位 \(k\),若在序列中存在 \(p\)\(a_{i},a_{i+1}\) 使得这两个数异或起来第 \(k\) 位为 \(1\),那么这个二进制位对代价的贡献为 \(p*c_k\)。序列总代价为所有二进制位代价和。输出这个最小代价。
\(n,\log_2 r\leq 50\)

和上一题有类似之处。

\(\max r< 2^k\)

对于一个限制 \([L,R]\),一个数必然是先和 \(L\)\(R\) 有一段 lcp,然后一位不同,然后接下来的可以随便填。

那么就可以进行区间 dp。设 \(f_{dep,l,r,S,T}\) 表示考虑二进制位 \([dep,k)\)\(l-1\) 的状态为 \(S\)\(r+1\) 的状态为 \(T\),区间 \([l,r]\) 的数在 \([1,dep)\) 内可以任意填的最小代价。特别地,要求 \(l-1,r+1\) 在第 \(dep\) 位时第一次变为小的位可以任意填的状态,那么这两个数的可行数量是很少的。具体地,只有至多 \(4\) 种:前面的位 \(=L_i\) 还是 \(R_i\);这一位有没有取反。那么状态就变为了 \(f_{dep,l,r,ld,le,rd,re}\)。答案是 \(f_{0,0,n+1,0,0,0,0}\)

考虑转移。什么状态可以转移到 \(f_{dep,l,r,ld,le,rd,re}\) 呢?

首先是,如果 \([l,r]\) 内没有任何一个数在 \(dep\) 层后取到“任意”的状态,那么应该从 \(f_{dep+1,l,r,*}\) 转移。具体地,因为 \(l-1,r+1\) 两个数在 \((dep,k)\) 处肯定是等于某一个边界的,所以应从 \(f_{dep+1,l,r,ld,0,rd,0}\) 转移。代价是什么?就是 \(l-1,r+1\)\(le,re\) 的影响下是否相同,如果不同会产生一个 \(c_{dep}\) 的代价(需特判,如果 \(l=1\)\(r=n\),那么即使不同也是不用代价的)。

其次,枚举一个 \(mid\) 使得 \(mid\)\(dep\) 层后取到“任意”的状态。再枚举 \(mid\) 前面的位等于 \(L\) 还是 \(R\) 的状态 \(p\)。那么 \(f_{dep,l,r,ld,le,rd,re}\) 应该从以下两个状态转移而来:

  • \(f_{dep,l, mid-1,ld,le,p,0} + f_{dep,mid+1, r, p, 0, rd, re}\)
  • \(f_{dep, l, mid-1, ld, le, p, 1} + f_{dep, mid+1, r, p, 1, rd, re}\)

如果可以从前者转移,考虑为什么 \(mid\) 的取值在 \(dep\) 位置依旧等于某一个界,但是后面的仍然可以任取?那么显然是 \(dep=0\)。因为后面根本没数位了。

如果可以从后者转移,要求 \(mid\) 的取值后几位是 \(000...0\) 或是 \(111...1\) 都在 \([L,R]\) 内,位运算计算一下即可。

复杂度 \(O(n^4)\)

点击查看代码
// Problem: E. XOR-ranges
// Contest: Codeforces - Codeforces Round 687 (Div. 1, based on Technocup 2021 Elimination Round 2)
// URL: https://codeforces.com/problemset/problem/1456/E
// Memory Limit: 256 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
//FASTIO need
const int N = 55;
int n, kk;
ll L[N], R[N];
ll C[N], f[N][N][N][2][2][2][2];

ll dp(int dep, int l, int r, int ld, int le, int rd, int re){
	if(dep == kk){
		return l > r ? 0 : 1e18;
	}
	if(f[dep][l][r][ld][le][rd][re] != -1){
		return f[dep][l][r][ld][le][rd][re];
	}
	ll &nw = f[dep][l][r][ld][le][rd][re];
	nw = 1e18;
	ll a = ((ld ? R[l-1] : L[l-1]) >> dep) ^ le;
	ll b = ((rd ? R[r+1] : L[r+1]) >> dep) ^ re;
	ll tmp = ((l == 1) || (r == n)) ? 0 : ((a & 1) ^ (b & 1));
	nw = min(nw, dp(dep+1, l, r, ld, 0, rd, 0) + tmp * C[dep]);
	for(int k = l; k <= r; ++ k){
		for(int p = 0; p <= 1; ++ p){
			if(dep == 0){
				nw = min(nw, dp(dep, l, k-1, ld, le, p, 0) + dp(dep, k+1, r, p, 0, rd, re));
			}	
			ll val = p ? R[k] : L[k];
			if(L[k] <= ((val^(1ll<<dep))&(~((1ll<<dep)-1))) && ((val^(1ll<<dep))|((1ll<<dep)-1)) <= R[k]){
				nw = min(nw, dp(dep, l, k-1, ld, le, p, 1) + dp(dep, k+1, r, p, 1, rd, re));
			}
		}
	}
	return nw;
}

int main(){
	read(n, kk);
	for(int i = 1; i <= n; ++ i){
		read(L[i], R[i]);
	}
	for(int i = 0; i < kk; ++ i){
		read(C[i]);
	}
	memset(f, -1, sizeof(f));
	println(dp(0, 1, n, 0, 0, 0, 0));
	return 0;
}

4. CF1644F - Basis \(\color{green}\texttt{战胜}\)

https://www.luogu.com.cn/problem/CF1644F。
题意:求一个最小的 \(m\),使得能够找到一个大小为 \(m\) 的序列集合 \(S\),满足所有长度为 \(n\),值域为 \([1,k]\) 的序列都能通过 \(S\) 中某一个序列进行若干次下面两种操作转换而成。
操作 1:任选一个 \(k\) 将序列 \(a\) 的每一个元素复制 \(p\) 遍,变为一个长度为 \(np\) 的序列后再取前 \(n\) 项。
操作 2:任选 \(x,y\),将序列中的 \(x\) 变为 \(y\)\(y\) 变为 \(x\)
\(n,k\leq 2e5\)

首先,如果没有操作 1,那么等价类的个数就应该是 \(\sum_{i=1}^{\min{n,k}}{n\brace i}\),即第二类斯特林数行的前缀。

如果有操作 \(1\) 怎么办?假设一个序列,它的每 \(d\) 个元素都是相同的(末尾不足 \(d\) 个也相同),那么它不会增加一个等价类,要减去。容易发现如果设原问题答案是 \(F(n)\),那么这个问题答案是 \(F(\lceil\dfrac nd\rceil)\),形成了一个可以递推的问题,可以使用数论分块+记忆化搜索优化。

打表可以发现,会用到的 \(F(>10000)\) 值数量很少,那么这部分 NTT 计算第二类斯特林数行,其它部分使用斯特林数递推公式计算即可通过。

点击查看代码
// Problem: F. Basis
// Contest: Codeforces - Educational Codeforces Round 123 (Rated for Div. 2)
// URL: https://codeforces.com/problemset/problem/1644/F
// Memory Limit: 512 MB
// Time Limit: 6000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
//FASTIO need
const int N = 2e5 + 10;
const ll P = 998244353;
int n, k;
int pr[N], pcc, vs[N], mu[N];

ll qp(ll x, ll y){
	ll ans = 1;
	while(y){
		if(y & 1){
			ans = ans * x % P;
		}
		x = x * x % P;
		y >>= 1;
	}
	return ans;
}

const int M = 1e4;
int st[M+10][M+10];

int tr[N*4], m;
const ll G = 3, iG = qp(3, P-2);
ll f[N*4], g[N*4], fac[N*4];
void ntt(ll *f, int n, bool op){
	for(int i = 0; i < n; ++ i){
		if(i < tr[i]){
			swap(f[i], f[tr[i]]);
		}
	}
	for(int p = 2; p <= n; p <<= 1){
		int len = p >> 1;
		ll tg = qp(op ? G : iG, (P-1) / p);
		for(int k = 0; k < n; k += p){
			ll buf = 1;
			for(int l = k; l < k + len; ++ l){
				int tmp = buf * f[l+len] % P;
				f[l+len] = (f[l] - tmp + P) % P;
				f[l] = (f[l] + tmp) % P;
				buf = buf * tg % P;
			}
		}
	}
}
ll calST(int n, int v){
	memset(f, 0, sizeof(f));
	memset(g, 0, sizeof(g));
	memset(tr, 0, sizeof(tr));
	fac[0] = 1;
  for(int i = 1; i <= n; ++ i){
      fac[i] = fac[i-1] * i % P;
  }
  for(int i = 0; i <= n; ++ i){
      f[i] = qp(i, n) * qp(fac[i], P-2) % P;
      g[i] = qp(P-1, i) * qp(fac[i], P-2) % P;
  }
  for(m = n + n, n = 1; n <= m; n <<= 1);
  for(int i = 0; i < n; ++ i){
      tr[i] = (tr[i>>1] >> 1) | ((i & 1) ? n >> 1 : 0);
  }
  ntt(f, n, 1);
  ntt(g, n, 1);
  for(int i = 0; i < n; ++ i){
      f[i] = f[i] * g[i] % P;
  }
  ntt(f, n, 0);
  ll ans = 0;
  for(int i = 1; i <= v; ++ i){
  	ans = (ans + f[i] * qp(n, P-2)) % P;
  }
  return ans;
}

ll ff[N];
ll calc(int n){
	if(ff[n] != -1){
		return ff[n];
	}
	ff[n] = 0;
	if(n > M){
		ff[n] = calST(n, min(n, k));
	} else {
		for(int i = 1; i <= min(n, k); ++ i){
			ff[n] = (ff[n] + st[n][i]) % P;
		}
	}
	for(int l = 2, r; l < n; l = r + 1){
		r = (n-1) / ((n-1) / l);
		ff[n] = (ff[n] + P - (r-l+1) * calc((n-1)/l+1) % P) % P;
	}
	if(n > 1){
		ff[n] = (ff[n] + P - calc(1)) % P;
	}
	return ff[n];
}

int main(){
	read(n, k);
	if(k == 1){
		println(1);
		return 0;
	}
	mu[1] = 1;
	st[0][0] = 1;
	for(int i = 1; i <= min(n, M); ++ i){
		for(int j = 1; j <= min(n, M); ++ j){
			st[i][j] = (st[i-1][j-1] + 1ll * st[i-1][j] * j) % P;
		}
	}
	for(int i = 2; i <= n; ++ i){
		if(!vs[i]){
			mu[i] = P - 1;
			pr[++pcc] = i;
		}
		for(int j = 1; i * pr[j] <= n && j <= pcc; ++ j){
			vs[i*pr[j]] = 1;
			if(i % pr[j]){
				mu[i*pr[j]] = P - mu[i];
			} else {
				mu[i*pr[j]] = 0;
				break;
			}
		}
	}
	memset(ff, -1, sizeof(ff));
	println(calc(n));
	return 0;
}

加强版:\(n\leq 2e6\),NTT 过不去了。其实可以发现是不用 NTT 的。具体地,将通项公式带入,推式子即可。复杂度 \(O(n\log n)\)

5. AT_jag2018summer_day2_k - Short LIS \(\color{red}\texttt{战败}\)

https://www.luogu.com.cn/problem/AT_jag2018summer_day2_k
统计排列 \(p\) 个数:最长上升子序列长度 \(\leq 2\)\(p_A=B\)
\(n\leq 1e6\)

最长上升子序列长度等价于最小下降子序列覆盖数。下降不是很好看,转化为最小上升子序列覆盖数 \(\leq 2\)

考虑把所有前缀最大值提出来,那么这个条件满足当且仅当剩余部分是一个上升子序列。那么对于怎样的前缀最大值序列,剩余部分可以构成上升子序列呢?充要条件是所有前缀最大值 \(p_i\geq i\)。证明是简单的。

然后考虑求这么一个前缀最大值 \((i,p_i)\) 集合。考虑一条折线,其中 \(x-1\to x\) 一段的纵坐标为 \(x\) 的前缀 \(\max\)。然后把垂直的线补上后,形如一条 \((0,0)\to (n,n)\) 的折线,每段平行的线的第一段表示更新一次前缀最大值。那么没有 \(p_A=B\) 的限制后就相当于卡特兰数。

如果有,那么限制是必须存在 \((A-1,B-1)\to(A-1,B)\to(A,B)\) 的两段折线,转化为 \((0,0)\to(A-1,B-1)\)\((A,B)\to(n,n)\) 两段折线的方案乘积,要求是折线不到 \(y=x\) 下方,使用反射容斥易求。

点击查看代码
// Problem: K - Short LIS
// Contest: AtCoder - Japan Alumni Group Summer Camp 2018 Day 2
// URL: https://atcoder.jp/contests/jag2018summer-day2/tasks/jag2018summer_day2_k
// Memory Limit: 1024 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
//FASTIO need
const int N = 2e6 + 10;
ll fac[N], inv[N];
const ll P = 1e9 + 7;
int n, a, b;

ll qp(ll x, ll y){
	ll ans = 1;
	while(y){
		if(y & 1){
			ans = ans * x % P;
		}
		x = x * x % P;
		y >>= 1;
	}
	return ans;
}
ll C(int n, int m){
	if(n < 0 || m < 0 || n < m){
		return 0;
	}
	return fac[n] * inv[m] % P * inv[n-m] % P;
}
ll calc(int n, int m){
	return (C(n+m, n) - C(n+m, n-1) + P) % P;
}

int main(){
	read(n, a, b);
	fac[0] = 1;
	int m = n * 2 + 5;
	for(int i = 1; i <= m; ++ i){
		fac[i] = fac[i-1] * i % P;
	}
	inv[m] = qp(fac[m], P-2);
	for(int i = m-1; i >= 0; -- i){
		inv[i] = inv[i+1] * (i+1) % P;
	}
	++ a;
	++ b;
	b = n - b + 1;
	if(b < a){
		swap(a, b);
	}
	println(calc(a - 1, b - 1) * calc(n - b, n - a) % P);
	return 0;
}

6. CF2089C2/THUPC2025 决赛 - Key of Like (Hard Version) \(\color{red}\texttt{战败}\)

https://www.luogu.com.cn/problem/CF2089C2
https://qoj.ac/problem/10289
题意:有 \(L\) 个锁,\(L+K\) 个钥匙,其中 \(L\) 个每一个对应着一个锁,另外 \(K\) 个是假的,以及 \(n\) 个人。\(n\) 个人从第一个开始,每个人执行以下操作:选择当前最有可能匹配的一把钥匙和一个锁,尝试是否匹配,若有多个最有可能匹配的组合,等概率选择其中一种;之后若匹配,拿去这个锁和钥匙。\(0,1,2,...,n-1,0,1,2,...,n-1,0,...\) 轮流进行,求最后每个人期望匹配成功多少次。
\(L\leq 5000, n,K\leq 50\)

考虑一下目前状态的决策,设有 \(l\) 个锁,\(l+k\) 个钥匙:

首先,对于第一个人,一定是随机选一个锁、一个钥匙,成功匹配概率 \(\dfrac 1{l+k}\)。若第一个人匹配失败,第二个人有三种决策:

  • 选择该锁和新钥匙,成功概率 \(\dfrac 1{l+k}\)
  • 选择该钥匙和新锁,成功概率不好算。但是感性理解,看作那 \(k\) 个钥匙对应了虚假的 \(k\) 个锁,成功概率依然是 \(\dfrac 1{l+k}\)
  • 选择新钥匙和新锁,成功概率小于上两种,不考虑。

对于第三个、第四个,直到成功匹配,所有人的策略都是和第二个人一样的,这个容易理解。而且,他们每一个人的成功概率都是 \(\dfrac 1{l+k}\)

一个例外:选择保留钥匙,但是这是把假钥匙。那么一定是 \(l\) 个人尝试匹配失败后得出的结论。出现概率是 \(\dfrac k{l+k}\)

那么,无论是成功匹配还是发现假钥匙,我们得到的信息只有一组匹配或者一个假钥匙,对于剩下的部分依旧是完全未知。这就使得我们可以规约到子问题进行求解。

因此,考虑动态规划。设 \(f_{l,k,m}\) 表示目前有 \(l\) 个锁,\(l+k\) 个钥匙,目前到了第 \(m\) 个人,第 \(0\) 个人之后期望成功匹配多少次。那么这时候把整个循环流程逆转过来,按照 \(m,m-1,...,0,n-1,n-2,...,0,...\) 的顺序,那么 \(f_{L,K,m}\) 的值就应该是原问题中第 \(m\) 个人的答案。

考虑转移(对于下文中状态的第三维,默认在 \(\bmod n\) 意义下):

  1. \(m\) 个人随机,成功。\(f_{l,k,m}\leftarrow (f_{l-1,k,m-1}+[m=0])\times \dfrac 1{l+k}\)。其中 \([m=0]\) 是因为在这个时候确确实实第 \(0\) 个人匹配了一次,期望应加上 \(1\) 乘以概率,下文中类似的部分同理。
  2. \(m-1\) 个人选择固定锁枚举钥匙,那么接下来的 \(m-1,m-2,...,m-(l+k-1)\) 所有人的成功概率都为 \(\dfrac 1{l+k}\)。若 \(m-p\) 成功了,应从 \(f_{l-1,k,m-p-1}\) 转移而来。\(f_{l,k,m}\leftarrow (f_{l-1,k,m-(i+1)}+[m=i])\times\dfrac 1{l+k}\dfrac {l+k-1}{l+l+k-2}\)。最后一部分是在固定锁还是固定钥匙中选到固定锁的方案的概率。
  3. \(m-1\) 个人选择固定钥匙枚举锁,那么接下来的 \(m-1,m-2,...,m-(l-1)\) 所有人的成功概率都为 \(\dfrac 1{l+k}\),与上面类似。\(f_{l,k,m}\leftarrow (f_{l-1,k,m-(i+1)}+[m=i])\times \dfrac 1{l+k}\dfrac{l-1}{l+l+k-2}\)
  4. 固定钥匙枚举锁,但是选中了假钥匙。\(f_{l,k,m}\leftarrow f_{l,k-1,m-l}\times\dfrac k{l+k}\dfrac{l-1}{l+l+k-2}\)

复杂度 \(O(nLK(L+K))\)。可以使用前缀和优化转移 \(2,3\),做到 \(O(nLK)\),通过此题。代码有正解有暴力。

点击查看代码
//qoj10289
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
//FASTIO need
//MODINT need
const int P = 1e9 + 7;
int n, L, K;
Modint<P> inv[20010];
Modint<P> f[5010][55][55];

int main(){
  int T = 1;
  n = 20000;
  for(int i = 1; i <= n; ++ i){
    inv[i] = i;
    inv[i] ^= P - 2;
  }
  // read(T);
  while(T--){
    read(n, L, K);
    for(int l = 1; l <= L; ++ l){
      for(int k = 0; k <= K; ++ k){
        Modint<P> lock = (l+k-1) * inv[l+l+k-2] * inv[l+k];
        Modint<P> key = (l-1) * inv[l+l+k-2] * inv[l+k];
        Modint<P> Slock = 0, Skey = 0;
        if(l + k >= 2){
          for(int i = 0; i < n; ++ i){
            Slock += f[l-1][k][i] * ((l+k-2) / n + (((l+k-2)%n) >= (-i-2+n+n)%n));
          }
        }
        if(l >= 2){
          for(int i = 0; i < n; ++ i){
            Skey += f[l-1][k][i] * ((l-2) / n + (((l-2)%n) >= (-i-2+n+n)%n));
          }
        }
        for(int m = 0; m < n; ++ m){
          f[l][k][m] = f[l-1][k][(m-1+n)%n] * inv[l+k];
          f[l][k][m] += key * k * f[l][k-1][(m-l%n+n)%n];
          if(m == 0){
            f[l][k][m] += inv[l+k];
          }
          f[l][k][m] += lock * Slock;
          Slock += f[l-1][k][(m-1+n)%n];
          Slock -= f[l-1][k][(m-(l+k)%n+n)%n];
          if(l + k >= 2){
            f[l][k][m] += lock * ((l+k-2) / n + ((l+k-2)%n >= (m-1+n)%n));
          }
          f[l][k][m] += key * Skey;
          Skey += f[l-1][k][(m-1+n)%n];
          Skey -= f[l-1][k][(m-l%n+n)%n];
          if(l >= 2){
            f[l][k][m] += key * ((l-2) / n + ((l-2)%n >= (m-1+n)%n));
          }
        }
      }
    }
    for(int i = 0; i < n; ++ i){
      printk(f[L][K][i].x);
    }
    println();
    for(int i = 0; i <= L; ++ i){
      for(int j = 0; j <= K; ++ j){
        for(int k = 0; k < n; ++ k){
          f[i][j][k].x = 0;
        }
      }
    }
  }
  return 0;
}

#ifdef BruteForce_Version
int main(){
  int T = 1;
  n = 20000;
  for(int i = 1; i <= n; ++ i){
    inv[i] = i;
    inv[i] ^= P - 2;
  }
  read(T);
  while(T--){
    read(n, L, K);
    for(int l = 1; l <= L; ++ l){
      for(int k = 0; k <= K; ++ k){
        for(int m = 0; m < n; ++ m){
          //l个锁,l+k个钥匙,m先操作,0位置的期望解锁数量。
          //m随机一个,开锁成功
          f[l][k][m] = f[l-1][k][(m-1+n)%n] * inv[l+k];
          if(m == 0) f[l][k][m] += inv[l+k];
          //固定一个锁
          //m-1,m-2,...,m-(l+k-1) 每一个人的成功概率都是 1/(l+k)
          Modint<P> val = (l+k-1) * inv[l+l+k-2] * inv[l+k];
          for(int i = 1; i <= (l+k-1); ++ i){
            f[l][k][m] += val * f[l-1][k][(m-(i+1)%n+n)%n];
            if((m - i) % n == 0){
              f[l][k][m] += val;
            }
          }
          //固定一个钥匙
          //m-1,m-2,...,m-(l-1) 每一个人的成功概率都是 1/(l+k)
          //或者是l-1次失败后发现钥匙是假的
          val = (l-1) * inv[l+l+k-2] * inv[l+k];
          for(int i = 1; i <= l - 1; ++ i){
            f[l][k][m] += val * f[l-1][k][(m-(i+1)%n+n)%n];
            if((m - i) % n == 0){
              f[l][k][m] += val;
            }
          }
          f[l][k][m] += val * k * f[l][k-1][(m-l%n+n)%n];
        }
      }
    }
    for(int i = 0; i < n; ++ i){
      printk(f[L][K][i].x);
    }
    println();
    for(int i = 0; i <= L; ++ i){
      for(int j = 0; j <= K; ++ j){
        for(int k = 0; k < n; ++ k){
          f[i][j][k].x = 0;
        }
      }
    }
  }
  return 0;
}
#endif

7. CF1477E - Nezzar and Tournaments \(\color{red}\texttt{战败}\)

https://www.luogu.com.cn/problem/CF1477E
题意:有 \((a_1,\texttt{A}),(a_2,\texttt{A}),...,(a_n,\texttt{A})\) 以及 \((b_1,\texttt{B}),(b_2,\texttt{B}),...,(b_m,\texttt{B})\),对于这 \(n+m\) 个二元组的任意一个排列可以按照如下方式求权值:假设第一维排成 \(c_1\sim c_{n+m}\),初始 \(x=k\),遍历 \(1\to {n+m}\),每次令 \(x\leftarrow \max(0, x+c_i-c_{i-1})\)(对于 \(i=1\)\(x\) 不变),然后若第二维为 \(\texttt{A}\)\(ans\leftarrow ans + x\),否则 \(ans\leftarrow ans-x\)。求所有排列中得到的 \(ans\) 最大值。支持单点修改,以及对于给定 \(k\) 求解。
\(n,m\leq 2e5, q\leq 5e5, a,b\leq 1e6\)

观察到 \(x\) 时刻对 \(0\)\(\max\) 是关键。如果没有这个,那么 \(x\) 将始终等于 \(x+c_i-c_1\)。那么考虑如果对 \(0\)\(\max\) 会变成什么样子。将 \(x+c_i-c_1\) 的折线画出来,求位置 \(i\)\(0\)\(\max\) 就相当于将这个点前面最小值提升至 \(0\),推导一下可知:

\[val_i=k-c_1+c_i+\max(0,c_1-k-\min_{j\leq i} c_j) \]

发现 \(c_1\) 是个特殊值,单独提出来考虑。尝试对于 \(c_2\sim c_{n+m}\) 找到一个通用的策略。对于 \(\texttt{A}\),要使它们尽量大,那么肯定是让那个 \(\min\) 尽可能小,于是递增排列;同理,\(\texttt{B}\) 应递降排列,二者结合,容易得到肯定是 \(\texttt B\) 放在 \(\texttt A\) 前面。那么我们得到了一个多项式复杂度的做法。剩下的只有考虑 \(c_1\) 能取到 \(a,b\) 中的哪个值。

\(f(x)\) 表示 \(c_1=x(x\in a)\)\(ans\) 的最大值。推导得:

\[f(x)=\left[(n-m)*k+\sum a-\sum b\right]+(m-n)x+(n-1)\max(0,x-k-\min\{a,b\})-\sum\max(0,x-k-b_i) \]

中括号内的为常数,不用管。然后发现这个是个分段一次函数,斜率在大部分情况为 \(m-n\to m-1\to m-2\to \cdots \to -1\),即先增大一次,后逐渐减小。那么最大值取在 \(x\) 最小的位置或者斜率为 \(0\) 的位置。具体地,\(x\) 只会取:

  1. \(\min\{a\}\)
  2. \(b\) 中次大值 \(+k\)\(a\) 中的前驱后继(如果 \(m=1\) 取最大值)。

\(g(x)\) 表示 \(c_1=x(x\in b)\)\(ans\) 的最大值。推导得:

\[g(x)=\left[(n-m)*k+\sum a-\sum b\right]+(m-n)x+n\max(0,x-k-\min\{a,b\})-\sum\max(0,x-k-b_i) \]

斜率为 \(m-n\to m\to m-1\to\cdots \to 0\)。所以只用取 \(\min\{b\},\max\{b\}\) 即可。

所以最后要求动态维护 \(F,G\),支持快速查值。发现难点只有维护 \(b\) 数组中小于某数数的和以及数量,还有单点加/删。使用权值线段树或者平衡树维护即可。

点击查看代码
const int N = 5e5 + 10;
int n, m, q;
ll mab = 1e18;
ll sum, a[N], b[N];

struct fhq{
  int ch[N][2], pri[N], siz[N], tot, rt = 0;
  ll sum[N], val[N];
  void upd(int p){
    siz[p] = siz[ch[p][0]] + siz[ch[p][1]] + 1;
    sum[p] = sum[ch[p][0]] + sum[ch[p][1]] + val[p];
  }
  int nnd(ll v){
    pri[++tot] = rand();
    val[tot] = sum[tot] = v;
    siz[tot] = 1;
    ch[tot][0] = ch[tot][1] = 0;
    return tot;
  }
  int mg(int x, int y){
    if(!x || !y){
      return x + y;
    }
    if(pri[x] < pri[y]){
      ch[x][1] = mg(ch[x][1], y);
      upd(x);
      return x;
    } else {
      ch[y][0] = mg(x, ch[y][0]);
      upd(y);
      return y;
    }
  }
  void sp(int p, ll k, int &x, int &y){
    if(!p){
      x = y = 0;
      return;
    }
    if(val[p] <= k){
      x = p;
      sp(ch[p][1], k, ch[p][1], y);
    } else {
      y = p;
      sp(ch[p][0], k, x, ch[p][0]);
    }
    upd(p);
  }
  void ins(ll v){
    int x, y;
    sp(rt, v, x, y);
    rt = mg(mg(x, nnd(v)), y);
  }
  void del(ll v){
    int x, y, z;
    sp(rt, v, x, z);
    sp(x, v-1, x, y);
    y = mg(ch[y][0], ch[y][1]);
    rt = mg(mg(x, y), z);
  }
  pair<int, ll> kth(int v){
    int x, y;
    sp(rt, v, x, y);
    pair<int, ll> rs = make_pair(siz[x], sum[x]);
    rt = mg(x, y);
    return rs;
  }
} tr;

multiset<ll> sa, sb;
ll C(ll x){
  pair<int, ll> tmp = tr.kth(x);
  return x * 1ll * tmp.first - tmp.second;
}
ll F(ll x, ll k){
  return x == -1 ? -1e18 : (m - n) * 1ll * x + max(0ll, x - k - mab) * 1ll * (n - 1) - C(x - k);
}
ll G(ll x, ll k){
  return x == -1 ? -1e18 : (m - n) * 1ll * x + max(0ll, x - k - mab) * 1ll * n - C(x - k);
}

int main(){
  int T = 1;
  while(T--){
    read(n, m, q);
    READ(a, n);
    for(int i = 1; i <= n; ++ i){
      sum += a[i];
      sa.insert(a[i]);
      mab = min(mab, a[i]);
    }
    READ(b, m);
    for(int i = 1; i <= m; ++ i){
      sum -= b[i];
      sb.insert(b[i]);
      mab = min(mab, b[i]);
      tr.ins(b[i]);
    }
    while(q--){
      int op;
      ll x = 114;
      read(op, x);
      if(op == 1){
        sum -= a[x];
        sa.erase(sa.lower_bound(a[x]));
        read(a[x]);
        sum += a[x];
        sa.insert(a[x]);
        mab = min(*sa.begin(), *sb.begin());
      } else if(op == 2){
        sum += b[x];
        sb.erase(sb.lower_bound(b[x]));
        tr.del(b[x]);
        read(b[x]);
        sum -= b[x];
        sb.insert(b[x]);
        mab = min(*sa.begin(), *sb.begin());
        tr.ins(b[x]);
      } else {
        ll A = *sa.begin();
        auto it = sb.end();
        ll tmp;
        if(m == 1){
          -- it;
        } else {
          -- it;
          -- it;
        }
        tmp = (*it) + x;
        auto ia = sa.lower_bound(tmp);
        ll B = ia == sa.end() ? -1 : *ia, C = -1;
        if(ia != sa.begin()){
          -- ia;
          C = *ia;
        }
        ll D = *sb.begin();
        it = sb.end();
        -- it;
        ll E = *it;
        ll ans = x * (n - m) + sum + max({F(A, x), F(B, x), F(C, x), G(D, x), G(E, x)});
        println(ans);
      }
    }
  }
  return 0;
}

8. CF1989F Simultaneous Coloring \(\color{red}\texttt{战败}\)

https://www.luogu.com.cn/problem/CF1989F
给定 \(n*m\) 的矩阵。你可以进行两种染色:将某一行染成红色,或将某一列染成蓝色。一次执行一个染色没有代价,一次执行多个染色的代价为 \(k^2\),其中 \(k\) 是染色数量。当多个染色同时执行时,对于行和列同时被染色的格子,其颜色可以任选,且不同格子的颜色可以不同。\(q\) 次询问,在每次询问前,所有格子是无色的。在最开始,没有对格子颜色的限制。在第 \(i\) 次询问,时额外要求第 \(x_i\) 行第 \(y_i\) 列的格子必须被染成颜色 \(c_i\),并计算满足当前所有限制的染色方案的最小代价。
\(n,m,q\leq 2e5\)

一个点被染红等价于那行染色时间 \(\leq\) 那列染色时间,蓝色则反之。可以建图。问题转化为对于边集的每个前缀,求 \(\sum_{scc} siz^2[siz>1]\)

求出每条边 \((u,v,t)\) 在时刻 \(t'\geq t\) 时两端形成强连通分量,那么时刻 \(i\) 的答案就是所有 \(t'\in[1,i]\) 的边都加入图中,图中的 \(>1\) 连通块大小平方和。

套路地考虑整体二分,设函数 \(calc(l,r,S)\) 表示已经确定 \(S\) 边集内的边的 \(t'\in[l,r]\)\(t'\in[1,l-1]\) 的边的强连通分量状态已经存入到并查集中,计算 \(ans_{[l,r]}\) 的答案。只用调用 \(calc(1,q+1,E)\) 即可。

具体流程如下:

  • 边界条件 \(S=\emptyset\)\(ans_{[l,r]}=ans_{l-1}\)
  • 边界条件 \(l=r\)。将 \(S\) 中边加入并查集,使用当前并查集更新 \(ans_l\)
  • 将所有 \([l,mid]\) 的边加入一张新图中(对于边 \((u,v)\),将 \(f_u\to f_v\) 加入图中,其中 \(f\) 为对应并查集的代表节点。这样就可以考虑上那些 \(t'\in[1,l)\) 的边的贡献了,同时保证了复杂度正确)。
  • 以所有有度数的点为根跑 tarjan。跑完后,若边两侧在同一 SCC 中,将这条边插入集合 \(left\);否则若在不同 SCC 或者边的 \(t>mid\),插入集合 \(right\)
  • 清空临时的新图。递归求解 \(calc(l, mid, left), calc(mid+1, r, right)\)
点击查看代码
const int N = 4e5 + 10;
int n, m, q;
struct eg{
  int u, v, tim;
} g[N];
ll ans[N], nans, on;

int fa[N], siz[N];

int gf(int x){
  return x == fa[x] ? x : fa[x] = gf(fa[x]);
}
void merge(int x, int y){
  x = gf(x);
  y = gf(y);
  if(x != y){
    nans -= 1ll * siz[x] * siz[x] + 1ll * siz[y] * siz[y];
    if(siz[x] == 1){
      -- on;
    }
    if(siz[y] == 1){
      -- on;
    }
    siz[x] += siz[y];
    nans += 1ll * siz[x] * siz[x];
    fa[y] = x;
  }
}

vector<int> G[N];
int dfn[N], scc[N], low[N], dfc, stk[N], stp, ins[N], sct;
void tj(int x){
  dfn[x] = low[x] = ++ dfc;
  stk[++stp] = x;
  ins[x] = 1;
  for(int i : G[x]){
    if(!dfn[i]){
      tj(i);
      low[x] = min(low[x], low[i]);
    } else if(ins[i]){
      low[x] = min(low[x], dfn[i]);
    }
  }
  if(low[x] == dfn[x]){
    ++ sct;
    while(stk[stp] != x){
      scc[stk[stp]] = sct;
      ins[stk[stp]] = 0;
      -- stp;
    }
    scc[stk[stp]] = sct;
    ins[stk[stp]] = 0;
    -- stp;
  }
}

void solve(int l, int r, vector<eg> &E){
  if(E.empty()){
    for(int i = l; i <= r; ++ i){
      ans[i] = nans - on;
    }
  } else if(l == r){
    for(auto i : E){
      merge(i.u, i.v);
    }
    ans[l] = nans - on;
  } else {
    int mid = l + r >> 1;
    vector<eg> le, ri;
    for(auto i : E){
      i.u = gf(i.u);
      i.v = gf(i.v);
      if(i.tim <= mid){
        G[i.u].push_back(i.v);
      }
    }
    for(auto i : E){
      i.u = gf(i.u);
      i.v = gf(i.v);
      if(i.tim <= mid){
        if(!dfn[i.u]){
          tj(i.u);
        }
        if(!dfn[i.v]){
          tj(i.v);
        }
        if(scc[i.u] == scc[i.v]){
          le.push_back(i);
        } else {
          ri.push_back(i);
        }
      } else {
        ri.push_back(i);
      }
    }
    dfc = 0;
    sct = 0;
    for(auto i : E){
      i.u = gf(i.u);
      i.v = gf(i.v);
      if(i.tim <= mid){
        dfn[i.u] = dfn[i.v] = 0;
        vector<int> ().swap(G[i.u]);
      }
    }
    solve(l, mid, le);
    solve(mid+1, r, ri);
  }
}

int main(){
  int T = 1;
  while(T--){
    read(n, m, q);
    vector<eg> nw;
    for(int i = 1; i <= n + m; ++ i){
      fa[i] = i;
      siz[i] = 1;
    }
    nans = on = n;
    for(int i = 1; i <= q; ++ i){
      int u, v;
      char ch[5];
      read(u, v);
      read_cstr(ch);
      v += n;
      if(ch[0] == 'R'){
        g[i] = {u, v, i};
      } else {
        g[i] = {v, u, i};
      }
      nw.push_back(g[i]);
    }
    solve(1, q + 1, nw);
    for(int i = 1; i <= q; ++ i){
      println(ans[i]);
    }
  }
  return 0;
}

9. CF1477D - Nezzar and Hidden Permutations \(\color{green}\texttt{战胜}\)

https://www.luogu.com.cn/problem/CF1477D
给定一个无向图,把它定向成 DAG,然后选取两个拓扑序使得对应位置相等的个数最少,输出拓扑序方案。
\(n,m\leq 5e5\)

取补图,补图中一个菊花子图,不妨设根为 \(x\) 点为 \(y_1,y_2,...,y_n\),那么 \(x,y_1,...,y_n\)\(y_1,...,y_n,x\) 都可以对应同一个拓扑序(因为这些点在原图中任意拓扑序中都没有先后限制)。

认为补图中的任何一个 \(siz>1\) 的连通块都可以进行菊花拆分。剩下的那些 \(siz=1\) 的连通块代表的点位置是固定的,无论如何都对答案有 \(1\) 的贡献。

如何进行菊花拆分?对于每个点 \(x\),找到一个 \(y\) 使得 \(x\to y\) 在补图上有这条边。若 \(y\) 没有出边,添加边 \((x,y)\);若 \(y\) 目前连通块大小为 \(2\),加入边 \((x,y)\) 构成大小为 \(3\) 的菊花(\(y\) 为根);若 \(y\) 是一个菊花的根,直接把 \(x\) 加入菊花;若 \(y\) 为一个菊花的叶子,将 \(y\) 拿出原菊花,\((x,y)\) 新建为一条边。

使用 set 维护每个菊花根的儿子集合。

点击查看代码
const int N = 5e5 + 10;
int n, m;
vector<int> g[N];
int ban[N], btp, to[N];
int st[N], fa[N];
set<int> s[N];
int X[N], Y[N], tt;

int main(){
  int T = 1;
  read(T);
  while(T--){
    read(n, m);
    for(int i = 1; i <= m; ++ i){
      int u, v;
      read(u, v);
      g[u].push_back(v);
      g[v].push_back(u);
    }
    btp = tt = 0;
    for(int i = 1; i <= n; ++ i){
      g[i].push_back(i);
      if(g[i].size() == n){
        ban[i] = 1;
      } else {
        sort(g[i].begin(), g[i].end());
        for(int j = 0; j < g[i].size(); ++ j){
          if(g[i][j] != j + 1){
            to[i] = j + 1;
            break;
          }
        }
        if(to[i] == 0){
          to[i] = g[i].size() + 1;
        }
        // println(i, to[i]);
        if(st[i] != 0){
          continue;
        }
        if(st[to[i]] == 0){
          st[to[i]] = st[i] = 1;
          fa[i] = to[i];
          fa[to[i]] = i;
        } else if(st[to[i]] == 1){
          st[i] = st[fa[to[i]]] = 3;
          fa[i] = to[i];
          st[to[i]] = 2;
          s[to[i]].insert(i);
          s[to[i]].insert(fa[to[i]]);
        } else if(st[to[i]] == 2){
          fa[i] = to[i];
          st[i] = 3;
          s[to[i]].insert(i);
        } else if(st[to[i]] == 3){
          int x = fa[to[i]];
          s[x].erase(to[i]);
          if(s[x].size() == 1){
            int y = *s[x].begin();
            s[x].erase(y);
            st[x] = st[y] = 1;
            fa[x] = y;
            fa[y] = x;
          }
          st[to[i]] = st[i] = 1;
          fa[i] = to[i];
          fa[to[i]] = i;
        }
      }
    }
    for(int i = 1; i <= n; ++ i){
      if(st[i] == 0){
        ++ tt;
        X[i] = Y[i] = tt;
      } else if(st[i] == 1){
        X[i] = tt+1;
        Y[i] = tt+2;
        X[fa[i]] = tt+2;
        Y[fa[i]] = tt+1;
        st[fa[i]] = st[i] = -1;
        tt += 2;
      } else if(st[i] == 2){
        X[i] = tt+1;
        int nw = tt+1;
        for(int j : s[i]){
          X[j] = ++ nw;
          Y[j] = nw - 1;
        }
        Y[i] = nw;
        tt = nw;
      }
    }
    for(int i = 1; i <= n; ++ i){
      printk(X[i]);
    }
    println();
    for(int i = 1; i <= n; ++ i){
      printk(Y[i]);
    }
    println();
    for(int i = 1; i <= n; ++ i){
      vector<int> ().swap(g[i]);
      st[i] = fa[i] = 0;
      s[i].clear();
      ban[i] = to[i] = X[i] = Y[i] = 0;
    }
    n = m = tt = btp = 0;
  }
  return 0;
}

10. CF1368G - Shifting Dominoes \(\color{red}\texttt{战败}\)

https://www.luogu.com.cn/problem/CF1368G
题意:给一个 \(n*m\) 的被 \(1*2\) 骨牌铺满的棋盘。现在操作:1.移去恰好一张骨牌。2.将一张骨牌沿着其长边进行移动,你可以进行这一步任意次。3.你需要保证在任意时刻,每张骨牌的位置与其初始位置至少有一个公共格子。求你可以得到的所有可能的局面的数量,两种局面不同,当且仅当某个位置在其中一者中被骨牌覆盖,而在另一者中没有。
\(n*m\leq 2e5\)

可以将移动的过程看作是空位的跳跃,那么建图,图形如一个外向基环树森林(实际上可以证明不存在环),后一个空位能够跳到的位置是树上一个子树。两个空位由于坐标 \(x+y\) 的奇偶性不同,所以永远不会走到相同位置。那么就相当于可以走到一个矩阵。扫描线维护矩阵并即可。

点击查看代码
const int N = 2e5 + 10;
int n, m;
char s[N];
vector<int> a[N];
vector<int> g[N];
int ind[N];
int dfn[N], siz[N], dfc;

void dfs(int x){
  siz[x] = 1;
  dfn[x] = ++ dfc;
  for(int i : g[x]){
    dfs(i);
    siz[x] += siz[i];
  }
}
vector<pair<int, int> > ad[N], de[N];

struct node{
  int cnt, mn, tag;
} t[N*4];
void add(int p, int l, int r, int ql, int qr, int v){
  if(qr < l || r < ql){
    return;
  } else if(ql <= l && r <= qr){
    t[p].mn += v;
    t[p].tag += v;
  } else {
    int mid = l + r >> 1;
    if(t[p].tag){
      t[p<<1].tag += t[p].tag;
      t[p<<1|1].tag += t[p].tag;
      t[p<<1].mn += t[p].tag;
      t[p<<1|1].mn += t[p].tag;
      t[p].tag = 0;
    }
    add(p<<1, l, mid, ql, qr, v);
    add(p<<1|1, mid+1, r, ql, qr, v);
    t[p].mn = min(t[p<<1].mn, t[p<<1|1].mn);
    t[p].cnt = 0;
    if(t[p].mn == t[p<<1].mn){
      t[p].cnt += t[p<<1].cnt;
    }
    if(t[p].mn == t[p<<1|1].mn){
      t[p].cnt += t[p<<1|1].cnt;
    }
  }
}
void build(int p, int l, int r){
  t[p].cnt = r - l + 1;
  if(l != r){
    int mid = l + r >> 1;
    build(p<<1, l, mid);
    build(p<<1|1, mid+1, r);
  }
}

int main(){
  int T = 1;
  #define cg(x, y) (((x) - 1) * m + (y))
  while(T--){
    read(n, m);
    for(int i = 1; i <= n; ++ i){
      read_cstr(s + 1);
      a[i].resize(m + 1);
      for(int j = 1; j <= m; ++ j){
        a[i][j] = s[j];
      }
    }
    for(int i = 1; i <= n; ++ i){
      for(int j = 1; j <= m; ++ j){
        if(a[i][j] == 'U'){
          if(i-1 >= 1 && i+1 <= n){
            g[cg(i-1,j)].push_back(cg(i+1, j));
            ++ ind[cg(i+1,j)];
          }
          if(i+2 <= n){
            g[cg(i+2,j)].push_back(cg(i, j));
            ++ ind[cg(i,j)];
          }
        } else if(a[i][j] == 'L'){
          if(j-1 >= 1 && j+1 <= m){
            g[cg(i,j-1)].push_back(cg(i, j+1));
            ++ ind[cg(i,j+1)];
          }
          if(j+2 <= m){
            g[cg(i,j+2)].push_back(cg(i, j));
            ++ ind[cg(i,j)];
          }
        }
      }
    }
    for(int i = 1; i <= n*m; ++ i){
      if(ind[i] == 0){
        dfs(i);
      }
    }
    for(int i = 1; i <= n; ++ i){
      for(int j = 1; j <= m; ++ j){
        if(a[i][j] == 'U'){
          int x = cg(i, j), y = cg(i+1, j);
          if((i + j) & 1){
            swap(x, y);
          }
          ad[dfn[x]].push_back(make_pair(dfn[y], dfn[y] + siz[y] - 1));
          de[dfn[x]+siz[x]-1].push_back(make_pair(dfn[y], dfn[y] + siz[y] - 1));
        } else if(a[i][j] == 'L'){
          int x = cg(i, j), y = cg(i, j+1);
          if((i + j) & 1){
            swap(x, y);
          }
          ad[dfn[x]].push_back(make_pair(dfn[y], dfn[y] + siz[y] - 1));
          de[dfn[x]+siz[x]-1].push_back(make_pair(dfn[y], dfn[y] + siz[y] - 1));
        }
      }
    }
    build(1, 0, n * m);
    long long ans = 0;
    for(int i = 1; i <= n * m; ++ i){
      for(auto j : ad[i]){
        add(1, 0, n * m, j.first, j.second, 1);
      }
      ans += n * m + 1 - t[1].cnt;
      for(auto j : de[i]){
        add(1, 0, n * m, j.first, j.second, -1);
      }
    }
    println(ans);
  }
  return 0;
}

11. P10591 - BZOJ4671 异或图 \(\color{red}\texttt{战败}\)

https://www.luogu.com.cn/problem/P10591
题意:给 \(s\)\(n\) 个点的图,求选择其中一个子集,使得图通过出现在这个子集中的图奇数次的边可以连通的方案数。
\(n\leq 10, s\leq 60\)

\(n\leq 10\) 的范围支持我们枚举连通块的形态。但是选定的连通块内连通是不好保证的,我们只能保证枚举一个划分 \(S_1,S_2,...,S_k\),位于不同的 \(S\) 的点之间没有边。

那么固定一组 \(S\) 如何求解?将两端不在同一个 \(S\) 内的边提出来,相当于 \(s\) 个二进制数异或起来为 \(0\),答案就是 \(2\) 的自由基数量次方。

因此,可以求出了 \(f_i\) 表示选定 \(i\) 个连通块的方案数。设 \(g_i\) 表示确实有 \(i\) 个连通块的方案数,那么 \(f,g\) 关系为 \(g_i=\sum_{j\geq i} f_j*s[i,j]\),其中 \(s[i,j]\) 为第二类斯特林数。

斯特林反演得 \(ans=g_1=\sum_{i=1}^n f_i(-1)^{i-1}S[i,1]=\sum_{i=1}^n (-1)^{i-1}(i-1)! f_i\)

12. JOI Open 2024 - 中暑 / Heat Stroke \(\color{green}\texttt{战胜}\)

https://www.luogu.com.cn/problem/P10627
题意:有 \(L\) 个医院,分别能容纳 \(a_1,a_2,...,a_L\) 个人。接下来 \(n\) 个时刻每个时刻会有一个线段 \((i,i+1)(1\leq i<L)\) 上产生一个中暑的人,他可以选择两侧任意一个医院入住,若都满则由直升机接走。求所有人选择医院的方案中,被直升机接走的人数最大值。
\(L,n\leq 8000\)

性质:每一段 \((i,i+1)\) 的人,一定是一段前缀的时间的人选择前往左边/右边,后面的人选择前往右边/左边,一段后缀的人没地方去。那么就可以设 \(f_{i,0/1,x,y}\) 表示考虑前 \(i\) 段,第 \(i\) 段的人的策略是先左边/先右边,两侧分别占据了 \(x,y\) 个人,有 \(cnt_i-x-y\) 个人被接走,此时被接走的人数最多有多少。

考虑转移。\(f_{i-1,0/1,x,y}\to f_{i,0/1,p,q}\) 合法的必要条件是:

  • \(y+p=a_i\) 或者 \(x+y=cnt_{i-1},p+q=cnt_i\),即考虑 \(a_i\) 是否满员。这个性质保证了此时转移的话,\(p,q\) 至多只用枚举一个。
  • \(y,p\) 中最后出现的那个人出现时间在 \(i-1,i\) 中最早的被接走的那个人的出现时间之前。注意最后出现的那个人具体是谁,要根据 \(y,p\) 是否为 \(0\) 分类讨论。

直接实现这个做法可以获得一个 \(O(n^3)\) 的做法,获得 \(74\) 分。

考虑优化:把式子写出来,将枚举 \(x\) 放到最内层,那么每一类转移都可以通过类似单调队列/前后缀和等方式优化至 \(O(1)\) 转移,从而做到 \(O(n^2)\)

代码写的有点诡j。

点击查看代码
const int N = 8010;
int n, c[N], m, in[N];
vector<int> bel[N];
vector<vector<int> > f[N][2];
int siz[N];

int main(){
	read(n);
	READ(c, n);
	read(m);
	READ(in, m);
	for(int i = 0; i < n; ++ i){
		bel[i].push_back(-1e9);
	}
	for(int i = 1; i <= m; ++ i){
		bel[in[i]].push_back(i);
	}
	for(int i = 0; i < n; ++ i){
		siz[i] = bel[i].size() - 1;
		f[i][0].resize(siz[i] + 1);
		f[i][1].resize(siz[i] + 1);
		for(int j = 0; j <= siz[i]; ++ j){
			f[i][0][j].resize(siz[i] + 1);
			f[i][1][j].resize(siz[i] + 1);
			for(int k = 0; k <= siz[i]; ++ k){
				f[i][0][j][k] = - 1e9;
				f[i][1][j][k] = - 1e9;
			}
		}
	}
	for(int i = 0; i < n; ++ i){
		bel[i].push_back(1e9);
	}
	f[0][0][0][0] = 0;
	for(int i = 1; i < n; ++ i){
		for(int j = 0; j <= siz[i-1]; ++ j){
			if(j + c[i] >= siz[i-1]){
				int k = siz[i-1] - j;
				for(int p = max(0, siz[i]-c[i]+k+1); p <= min(siz[i], c[i+1]); ++ p){
					f[i][1][siz[i]-p][p] = max(f[i][1][siz[i]-p][p], f[i-1][0][j][k]);
					f[i][1][siz[i]-p][p] = max(f[i][1][siz[i]-p][p], f[i-1][1][j][k]);
				}
				if(siz[i] + k < c[i]){
					f[i][0][siz[i]][0] = max(f[i][0][siz[i]][0], f[i-1][0][j][k]);
					f[i][0][siz[i]][0] = max(f[i][0][siz[i]][0], f[i-1][1][j][k]);
				}
			}
		}
		
		for(int k = 0; k <= siz[i-1] && k <= c[i]; ++ k){
			#define OK(j) ((k?bel[i-1][j+k]:0) < bel[i][p+c[i]-k+1] && (c[i]-k?bel[i][p+c[i]-k]:0) < bel[i-1][j+k+1])
			#define CAL(j) (f[i-1][0][j][k] + siz[i] - c[i] + k)
			static int qu[N];
			int jr = -1, L = 0, R = -1;
			for(int p = 0; p <= min(siz[i]-c[i]+k, c[i+1]); ++ p){
				while(jr < siz[i-1] - k && OK(jr+1)){
					++ jr;
					while(L <= R && CAL(qu[R]) <= CAL(jr)){
						-- R;
					}
					qu[++R] = jr;
				}
				while(L <= R && !OK(qu[L])) ++ L;
				int val = L <= R ? CAL(qu[L]) : -1e9;
				f[i][1][c[i]-k][p] = max(f[i][1][c[i]-k][p], val - p);
			}
		}
					
		for(int k = 0; k <= siz[i-1] && k <= c[i]; ++ k){
			int nj = siz[i-1] - k + 1, val = -1e9;
			for(int p = min(c[i+1], siz[i] + k - c[i]); p >= 0; -- p){
				if((k?bel[i-1][k]:0) >= bel[i][p+c[i]-k+1]) continue;
				while(nj > 0 && (c[i]-k?bel[i][p+c[i]-k]:0) < bel[i-1][nj+k]){
					-- nj;
					val = max(val, f[i-1][1][nj][k] + siz[i] - c[i] + k);
				}
				f[i][1][c[i]-k][p] = max(f[i][1][c[i]-k][p], val - p);
			}
		}
				
		for(int k = max(0, c[i] - siz[i]); k <= siz[i-1] && k <= c[i]; ++ k){
			int jr = -1, jl = siz[i-1] - k + 1, val = -1e9;
			#define CAL(j) (f[i-1][0][j][k] + siz[i] - c[i] + k)
			while(jl > 0 && (c[i]-k?bel[i][c[i]-k]:0) < bel[i-1][jl+k]){
				-- jl;
			}
			for(int p = 0; p <= c[i+1] && p + c[i]-k <= siz[i]; ++ p){
				int pjr = jr, pjl = jl;
				while(jr < siz[i-1] - k && (k?bel[i-1][jr+1+k]:0) < bel[i][p+c[i]-k+1]){
					++ jr;
					if(jr >= jl) val = max(val, CAL(jr));
				}
				f[i][0][c[i]-k][p] = max(f[i][0][c[i]-k][p], val - p);
			}
		}
		for(int k = max(0, c[i] - siz[i]); k <= siz[i-1] && k <= c[i]; ++ k){
			int nj = siz[i-1] - k + 1, val = -1e9;
			for(int p = min(c[i+1], siz[i] + k - c[i]); p >= 0; -- p){
				if((k?bel[i-1][k]:0) >= bel[i][p+c[i]-k+1]) continue;
				while(nj > 0 && (c[i]-k?bel[i][p+c[i]-k]:0) < bel[i-1][nj+k]){
					-- nj;
					val = max(val, f[i-1][1][nj][k] + siz[i] - c[i] + k);
				}
				f[i][0][c[i]-k][p] = max(f[i][0][c[i]-k][p], val - p);
			}
		}
	}
	int ans = -1e9;
	for(int i = 0; i <= c[n-1] && i <= siz[n-1]; ++ i){
		for(int j = 0; i + j <= siz[n-1] && j <= c[n]; ++ j){
			if(i + j == siz[n-1] || j == c[n]){
				ans = max(ans, f[n-1][0][i][j]);
				ans = max(ans, f[n-1][1][i][j]);
			}
		}
	}
	println(ans);
	return 0;
}

13. PKUWC2018 - 猎人杀 \(\color{red}\texttt{战败}\)

https://www.luogu.com.cn/problem/P5644
一开始有 \(n\) 个猎人,第 \(i\) 个猎人有仇恨度 \(w_i\),每个猎人只有一个固定的技能:死亡后必须开一枪,且被射中的人也会死亡。然而向谁开枪也是有讲究的,假设当前还活着的猎人有 \([i_1\ldots i_m]\),那么有 \(\frac{w_{i_k}}{\sum_{j = 1}^{m} w_{i_j}}\) 的概率是向猎人 \(i_k\) 开枪。一开始第一枪由你打响,目标的选择方法和猎人一样(即有 \(\frac{w_i}{\sum_{j=1}^{n}w_j}\) 的概率射中第 \(i\) 个猎人)。由于开枪导致的连锁反应,所有猎人最终都会死亡,现在 \(1\) 号猎人想知道它是最后一个死的的概率。
\(1\leq w_i,\sum w_i\leq 1e5\)

经典性质:按加权概率选择一个活着的人等价于多次按加权概率选择任何一个人,直到选择到一个活着的人。

经典 trick:考虑容斥,设 \(S\) 内的人在 \(1\) 之后死亡的概率为 \(p(S)\),那么答案:

\[ans=\sum_S (-1)^{|S|}p(S) \]

\(p(S)\) 的计算是简单的:任意多次选择除掉 \(S,1\) 以外的人后选到一次 \(1\) 的概率:

\[p(S)=\sum_{i=0}^{\infin}(\dfrac{\sum w - w_1-\sum_{i\in S}w_i}{\sum w})^i\times \dfrac{w_1}{\sum w}=\dfrac{w_1}{w_1+\sum_{i\in S}w_i} \]

使用分治+NTT 求出 \(F(x)\) 表示 \(\sum_{i\in S} w_i=x\)\((-1)^{|S|}\) 和即可。

点击查看代码
const int N = 1e6 + 10;
int n, p[N], tot;
const ll P = 998244353;

ll qp(ll x, ll y){
	ll ans = 1;
	while(y){
		if(y & 1){
			ans = ans * x % P;
		}
		x = x * x % P;
		y >>= 1;
	}
	return ans;
}
const ll G = 3, iG = qp(3, P-2);

int tr[N], nw;
void tpre(int n){
	if(nw != n){
		nw = n;
		for(int i = 0; i < n; ++ i){
			tr[i] = (tr[i>>1] >> 1) | ((i & 1) ? (n >> 1) : 0);
		}
	}
}
unsigned long long ff[N], w[N];
struct poly{
	vector<ll> v;
	int hi = 0;
	void ntt(int op, int n){
		w[0] = 1;
		for(int i = 0; i < n; ++ i){
			ff[i] = v[tr[i]];
		}
		for(int p = 2; p <= n; p <<= 1){
			int len = p >> 1;
			ll tg = qp(op ? G : iG, (P-1) / p);
			for(int i = 1; i < len; ++ i){
				w[i] = w[i-1] * tg % P;
			}
			for(int k = 0; k < n; k += p){
				for(int l = 0; l < len; ++ l){
					ll tmp = w[l] * ff[l+k+len] % P;
					ff[l+k+len] = ff[l+k] + P - tmp;
					ff[l+k] += tmp;
				}
			}
      if(p == (1 << 11)){
				for(int i = 0; i < n; ++ i){
					ff[i] %= P;
				}
      }
		}
		if(!op){
			ll invn = qp(n, P-2);
			for(int i = 0; i < n; ++ i){
				v[i] = ff[i] % P * invn % P;
			}
		} else {
			for(int i = 0; i < n; ++ i){
				v[i] = ff[i] % P;
			}
		}
	}
} a[N], F;

poly mul(int l, int r){
	if(l == r){
		return a[l];
	}
	int mid = l + r >> 1;
	poly f = mul(l, mid), g = mul(mid+1, r);
	int n = f.hi + g.hi + 1;
	int m = 1;
	while(m < n){
		m *= 2;
	}
	tpre(m);
	poly tmp;
	f.hi = g.hi = tmp.hi = m - 1;
	f.v.resize(m);
	g.v.resize(m);
	tmp.v.resize(m);
	f.ntt(1, m);
	g.ntt(1, m);
	for(int i = 0; i < m; ++ i){
		tmp.v[i] = f.v[i] * g.v[i] % P;
	}
	tmp.ntt(0, m);
	return tmp;
}

int main(){
	read(n);
	READ(p, n);
	for(int i = 2; i <= n; ++ i){
		tot += p[i];
		a[i].hi = p[i];
		a[i].v.resize(p[i] + 1);
		a[i].v[0] = 1;
		a[i].v[p[i]] = P-1;
	}
	F = mul(2, n);
	ll ans = 0;
	for(int i = 0; i <= tot; ++ i){
		ans = (ans + F.v[i] * p[1] % P * qp(p[1] + i, P-2)) % P;
	}
	println(ans);
	return 0;
}

14. PKUWC2018 - 随机游走 \(\color{green}\texttt{战胜}\)

https://www.luogu.com.cn/problem/P5643
题意:给你一棵树,多次询问,每次给定点集 \(S\),求从根开始随机游走,走完所有 \(S\) 内点的期望步数。
\(n\leq 18\)

min-max 容斥。\(\max S=\sum_{T\subseteq S}(-1)^{|T|-1}\min T\),问题转化为对于所有集合,根到集合内任意一点随机游走的期望步数。

设枚举到集合 \(T\)\(f_i\) 表示点 \(i\)\(T\) 内任意一点随机游走的期望步数。观察到 \(f_i\) 是关于 \(f_{fa_i}\) 的一次函数,于是设 \(f_i=k_if_{fa_i}+b_i\) 即可一遍树形 dp 求出答案。

之后使用 fwt or 合并即可。

点击查看代码
const int N = 20, M = 1 << 18;
const ll P = 998244353;
int n, Q, rt;
vector<int> g[N];
ll k[N], b[N], inv[N];
ll f[M];

ll qp(ll x, ll y){
	ll ans = 1;
	while(y){
		if(y & 1){
			ans = ans * x % P;
		}
		x = x * x % P;
		y >>= 1;
	}
	return ans;
}
void dfs(int x, int fa, int T){
	if(T & (1 << x - 1)){
		k[x] = b[x] = 0;
		return;
	}
	k[x] = b[x] = g[x].size();
	for(int i : g[x]){
		if(i == fa){
			continue;
		}
		dfs(i, x, T);
		k[x] = (k[x] + P - k[i]) % P;
		b[x] = (b[x] + b[i]) % P;
	}
	k[x] = qp(k[x], P-2);
	b[x] = b[x] * k[x] % P;
}

int main(){
	read(n, Q, rt);
	for(int i = 1; i < n; ++ i){
		inv[i] = qp(i, P-2);
		int u, v;
		read(u, v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	for(int T = 1; T < (1 << n); ++ T){
		dfs(rt, 0, T);
		if(__builtin_popcount(T) & 1){
			f[T] = b[rt];
		} else {
			f[T] = (P - b[rt]) % P;
		}
		// println(T, f[T]);
	}
	for(int i = 0; i < n; ++ i){
		for(int T = 0; T < (1 << n); ++ T){
			if(T & (1 << i)){
				f[T] = (f[T] + f[T^(1<<i)]) % P;
			}
		}
	}
	while(Q--){
		int sz, x;
		int val = 0;
		read(sz);
		while(sz--){
			read(x);
			val |= 1 << (x - 1);
		}
		println(f[val]);
	}
	return 0;
}

15. CF1942H - Farmer John's Favorite Intern \(\color{blue}\texttt{讲题}\)

https://www.luogu.com.cn/problem/CF1942H
题意:有两种操作:操作 1:给定 \(x\)\(v\) 次从它的子树或者它的父亲中选择一个点点权 \(+1\);操作 2:给定 \(x\)\(v\) 次从它的子树中选择一个点点权 \(-1\)。对于每一个操作前缀,求是否有一种重排所有操作以及任意指定选择的点的方式,使得操作结束后点 \(i\) 的点权 \(>b_i\)
\(n,q\leq 2e5\)

考虑没有修改,相当于一个匹配:对于每次执行在 \(x\) 处的操作 \(2\)(定义总共有这么 \(c_x\) 次操作),需要与在 \(x\) 子树内或 \(x\) 根链内的一个操作 \(1\) 匹配;对于最后在 \(x\) 处的限制 \(b_x\),需要与在 \(x\) 儿子集合内或 \(x\) 根链内的 \(b_x\) 个操作 \(1\) 匹配。

那么考虑 hall 定理,则存在答案当且仅当对于任何集合 \(S,T\),有 \(\sum_{i\in S}b_i+\sum_{i\in T}c_i\geq \sum_{i\in N_b(S)\cup N_c(T)} a_i\)。其中 \(N_b(S)=\bigcup_{i\in S} \mathbb{anc}(i)\cup\mathbb{son}(i), N_c(S)=\bigcup_{i\in S}\mathbb{anc}(i)\cup\mathbb{subtree}(i)\)

考虑树形 dp 求出上式左侧减去右侧的最小值。设 \(f_{u,0/1}\) 表示考虑 \(u\) 子树内每个点是否在 \(S,T\) 中,\(u\)\(a\) 被选了 / \(u\) 子树内所有 \(a\) 都被选了。分类讨论 \(u\) 是否在 \(S,T\) 中(定义 \(a_x\) 表示执行在 \(x\) 处的操作 \(1\) 次数):

  • \(u\) 不在 \(S,T\) 中:\(f_{u,0}\leftarrow a_u + \sum \min(0, f_{v,0})\)。与 \(0\)\(\min\) 是因为,可以 \(v\) 的子树内一个点都没有被选,这样 \(a_v\) 也就不用被选了。
  • \(u\)\(S\) 中不在 \(T\) 中:\(f_{u,0}\leftarrow a_u-b_u + \sum f_{v,0}\)。不与 \(0\)\(\min\) 是因为,\(u\in S\)\(u\) 的每个儿子都在邻域中。
  • \(u\)\(S,T\) 中:\(f_{u,0},f_{u,1}\leftarrow a_u-b_u-c_u + \sum f_{v,1}\)。因为 \(u\in T\)\(u\) 的子树内每个点都在邻域中。
  • \(u\)\(T\) 中不在 \(S\) 中,此时一定不优于 \(u\)\(S,T\) 中。

得到了一个 \(O(nq)\) 的做法。容易发现可以动态 dp 优化。设 \(f_{u,2} = \max(f_{u,0}, f_{u,1})\),那么每个转移形如:

\[f_{u,p}=\max\{\sum_v f_{v,q} + val_{u,p,q}\} \]

树剖,每个点的矩阵表示它的重儿子的状态乘以这个矩阵可以得到它自己的状态(即将轻儿子的状态放入矩阵中)。使用线段树维护区间矩阵乘法,另外维护每个点的轻儿子 dp 数组和、每个链顶的 dp 数组,即可每次修改 \(O(\log n)\) 个矩阵完成对于 \(a,c\) 数组的单点修改。由于 \(1\) 是链顶,\(f_{1,0}\) 被实时维护,此时判断这个数是否 \(\geq 0\) 即可。

总复杂度 \(O(64n\log^2 n)\),跑了 3s。

点击查看代码
const int N = 2e5 + 10;
int n, q, b[N];
vector<int> g[N];
ll a[N], c[N];

int dfn[N], dfc, dep[N], siz[N], anc[N], son[N], top[N], fdf[N], btm[N];
void dfs(int x, int fa){
  dep[x] = dep[fa] + 1;
  anc[x] = fa;
  siz[x] = 1;
  for(int i : g[x]){
    dfs(i, x);
    siz[x] += siz[i];
    if(siz[son[x]] < siz[i]){
      son[x] = i;
    }
  }
}
void dfss(int x, int tp){
  top[x] = tp;
  dfn[x] = ++ dfc;
  fdf[dfc] = x;
  if(son[x]){
    dfss(son[x], tp);
    btm[x] = btm[son[x]];
  } else {
    btm[x] = x;
  }
  for(int i : g[x]){
    if(i != son[x]){
      dfss(i, i);
    }
  }
}

struct mat{
  ll a[4][4];
  mat operator * (const mat &b) const {
    mat c;
    memset(c.a, 0x3f, sizeof(c.a));
    for(int k = 0; k < 4; ++ k){
      for(int i = 0; i < 4; ++ i){
        for(int j = 0; j < 4; ++ j){
          c.a[i][j] = min(c.a[i][j], a[i][k] + b.a[k][j]);
        }
      }
    }
    return c;
  }
} t[N*4];

void mdf(int p, int l, int r, int x, mat &v){
  if(l == r){
    t[p] = v;
  } else {
    int mid = l + r >> 1;
    if(x <= mid){
      mdf(p<<1, l, mid, x, v);
    } else {
      mdf(p<<1|1, mid+1, r, x, v);
    }
    t[p] = t[p<<1] * t[p<<1|1];
  }
}
mat ask(int p, int l, int r, int ql, int qr){
  if(ql <= l && r <= qr){
    return t[p];
  } else {
    int mid = l + r >> 1;
    if(qr <= mid){
      return ask(p<<1, l, mid, ql, qr);
    } else if(mid < ql){
      return ask(p<<1|1, mid+1, r, ql, qr);
    } else {
      return ask(p<<1, l, mid, ql, qr) * ask(p<<1|1, mid+1, r, ql, qr);
    }
  }
}
ll f[N][3], qing[N][3];
mat now[N];
const ll inf = 1e18;

void calc(){
  memset(qing, 0, sizeof(qing));
  for(int i = n; i >= 1; -- i){
    int x = fdf[i];
    memset(now[x].a, 0x3f, sizeof(now[x].a));
    now[x].a[0][0] = now[x].a[0][2] = a[x] - b[x] + qing[x][0];
    now[x].a[1][0] = now[x].a[1][1] = now[x].a[1][2] = a[x] - b[x] - c[x] + qing[x][1];
    now[x].a[2][0] = now[x].a[2][2] = a[x] + qing[x][2];
    now[x].a[3][2] = now[x].a[3][3] = 0;
    mdf(1, 1, n, n-dfn[x]+1, now[x]);
    if(top[x] == x){
      mat tmp = ask(1, 1, n, n-dfn[btm[x]]+1, n-dfn[x]+1);
      for(int p = 0; p < 3; ++ p){
        f[x][p] = inf;
        for(int q = 0; q < 4; ++ q){
          f[x][p] = min(f[x][p], tmp.a[q][p]);
        }
      }
      qing[anc[x]][0] += f[x][0];
      qing[anc[x]][1] += f[x][1];
      qing[anc[x]][2] += f[x][2];
    }
  }
}
void mdf(int x){
  while(true){
    memset(now[x].a, 0x3f, sizeof(now[x].a));
    now[x].a[0][0] = now[x].a[0][2] = a[x] - b[x] + qing[x][0];
    now[x].a[1][0] = now[x].a[1][1] = now[x].a[1][2] = a[x] - b[x] - c[x] + qing[x][1];
    now[x].a[2][0] = now[x].a[2][2] = a[x] + qing[x][2];
    now[x].a[3][2] = now[x].a[3][3] = 0;
    mdf(1, 1, n, n-dfn[x]+1, now[x]);
    int y = top[x];
    qing[anc[y]][0] -= f[y][0];
    qing[anc[y]][1] -= f[y][1];
    qing[anc[y]][2] -= f[y][2];
    mat tmp = ask(1, 1, n, n-dfn[btm[y]]+1, n-dfn[y]+1);
    for(int p = 0; p < 3; ++ p){
      f[y][p] = inf;
      for(int q = 0; q < 4; ++ q){
        f[y][p] = min(f[y][p], tmp.a[q][p]);
      }
    }
    if(y == 1){
      return;
    }
    qing[anc[y]][0] += f[y][0];
    qing[anc[y]][1] += f[y][1];
    qing[anc[y]][2] += f[y][2];
    x = anc[y];
  }
}

int main(){
  int T;
  read(T);
  while(T--){
    read(n, q);
    for(int i = 2; i <= n; ++ i){
      int p;
      read(p);
      g[p].push_back(i);
    }
    READ(b, n);
    dfs(1, 0);
    dfss(1, 1);
    calc();
    while(q--){
      int op, x, v;
      read(op, x, v);
      if(op == 1){
        a[x] += v;
      } else {
        c[x] += v;
      }
      mdf(x);
      println_cstr(f[1][0] >= 0 && f[1][1] >= 0 ? "YES" : "NO");
    }
    for(int i = 1; i <= n; ++ i){
      vector<int> ().swap(g[i]);
      a[i] = b[i] = c[i] = 0;
      dfn[i] = dep[i] = siz[i] = anc[i] = son[i] = top[i] = 0;
      f[i][0] = f[i][1] = f[i][2] = 0;
      qing[i][0] = qing[i][1] = qing[i][2] = 0;
      memset(now[i].a, 0, sizeof(now[i].a));
    }
    dfc = 0;
  }
  return 0;
}

16. P9839 - 四暗刻单骑 \(\color{red}\texttt{战败}\)

https://www.luogu.com.cn/problem/P9839
题意:两个人,初始手牌分别是 \(x,y\),牌堆由上至下为 \(a_l,...,a_r\),两人交替摸牌,摸一张打一张,若摸到的牌和手上的牌相同,或另一方打出的牌与手上的牌相同,则获胜。多次询问 \(x,y,l,r\),问先手胜/后手胜/平局。
\(n\leq 2e5\)

牛气冲天题。

若两人手牌相同,那么策略是固定的:一直摸直到摸到一张与手牌相同的。

对于一个人手上的一张 \(x\) 牌,贡献有:

  1. 后面没有 \(x\) 牌,没有贡献。
  2. 后面一张 \(x\) 牌,位置 \(i\),归这个人拿。贡献是在 \(i\) 处直接获胜。
  3. 后面一张 \(x\) 牌,位置 \(i\),不归这个人拿,再后面没有 \(x\) 了。贡献是在 \(i\) 处平局。
  4. 后面两张 \(x\) 牌,位置 \(i,j\),都不归这个人拿,贡献是在 \(i\) 处直接失败。
  5. 后面两张 \(x\) 牌,位置 \(i,j\)\(i\) 不归这个人拿,\(j\) 归这个人拿,贡献是在 \(i\) 处直接获胜。

于是得到了一个暴力的做法:每次优先选择能够最快获胜的牌,没有获胜牌选择则选择平局,再没有选择能够最慢失败的牌。复杂度 \(O(nm)\)

发现:实际上贡献 4 是几乎不会发生的,因为这个人完全不会选这个有失败贡献的 \(x\) 牌,唯一的特例是 \(x\) 实际上是这个人的初始手牌。再讨论一下,发现大部分情况,若初始手牌是失败的也会将初始手牌打掉,最后剩下两种情况:

  1. \(x=y\)。简单特判即可。
  2. \(y=a_l\)。若先手保留 \(x\),则 \(y\) 打不打都可以,转化成不会发生贡献 4 的情况;否则转化成上一种。

那么没有贡献 4,最先获胜就是所有合法的 \((i,j)\) 中,最小的 \(j\) 对应的 \(i\) 归谁拿。

考虑什么样的 \((i,j)\) 合法(\(j,k\) 分别是 \(i,j\) 后第一个等于 \(a_i\) 的位置):

  1. \(i\equiv j(\bmod 2)\),若 \(i\in[l,r], j \leq r\) 即可。
  2. 否则若 \(i\equiv k(\bmod 2)\),若 \(i\in[l,r],k\leq r\) 即可。

发现随着询问的 \(r\) 增大,合法的区间会增多。那么对 \(r\) 扫描线,按顺序加入对应区间即可。

最后求得一组最早胜利时间 \(t\) 以及对应胜者。那么败者能否做到平局?考虑合法的平局 \((i,j)\),应该满足:

  1. \(r<k\),若 \(i\in[l,r],j\leq r\) 即可。

那么随着 \(r\) 减小,合法的平局区间会增多,对 \(r\) 逆序扫描线,可以求出败者的最小平局时间 \(t'\)。若 \(t' < t\),则败者能做到平局,否则胜者获胜。

最后记得对 \(y=a_l\) 特判,先手可能会有更多获胜机会。

点击查看代码
const int N = 2e5 + 10;
int n, Q, V, a[N];
struct qry{
  int x, y, l, r;
} qr[N];
vector<int> lpos[N], rpos[N];
int occ[N], nxt[N];
int nxx[N], nxy[N];
int ans[N];

bool chkwin(int o, int p, int q, int r){
  if(p <= r && (p&1) == (o&1)){
    return 1;
  } else if(p <= r && (p&1) != (o&1) && q <= r && (q&1) == (o&1)){
    return 1;
  }
  return 0;
}
bool chkpj(int o, int p, int q, int r){
  if(q > r && (o&1) != (p&1)){
    return 1;
  }
  return 0;
}

struct segtree{
  int t[N*4];
  void add(int p, int l, int r, int x, int v){
    if(l == r){
      t[p] = min(t[p], v);
    } else {
      int mid = l + r >> 1;
      if(x <= mid){
        add(p<<1, l, mid, x, v);
      } else {
        add(p<<1|1, mid+1, r, x, v);
      }
      t[p] = min(t[p<<1], t[p<<1|1]);
    }
  }
  int ask(int p, int l, int r, int ql, int qr){
    if(qr < l || r < ql){
      return 1e9;
    } else if(ql <= l && r <= qr){
      return t[p];
    } else {
      int mid = l + r >> 1;
      return min(ask(p<<1, l, mid, ql, qr), ask(p<<1|1, mid+1, r, ql, qr));
    }
  }
} t1, t2, t3, t4;
int mnn[N], mnpp[N];
vector<int> add[N], addd[N];
int apjj[N], bpjj[N];

int main(){
  memset(t1.t, 0x3f, sizeof(t1.t));
  memset(t2.t, 0x3f, sizeof(t2.t));
  memset(t3.t, 0x3f, sizeof(t3.t));
  memset(t4.t, 0x3f, sizeof(t4.t));
  read(n, Q, V);
  READ(a, n);
  for(int i = 1; i <= Q; ++ i){
    int x, y, l, r;
    read(x, y, l, r);
    qr[i] = {x, y, l, r};
    lpos[l].push_back(i);
    rpos[r].push_back(i);
  }
  for(int i = 1; i <= V; ++ i){
    occ[i] = n + 1;
  }
  nxt[n+1] = n+1;
  for(int i = n; i >= 1; -- i){
    nxt[i] = occ[a[i]];
    occ[a[i]] = i;
    for(int id : lpos[i]){
      nxx[id] = occ[qr[id].x];
      nxy[id] = occ[qr[id].y];
    }
  }
  for(int i = 1; i <= n; ++ i){
    int o = i, p = nxt[o], q = nxt[p];
    int rlim = n + 1;
    if((p & 1) == (o & 1)){
      rlim = p;
    } else if((q & 1) == (o & 1)){
      rlim = q;
    }
    add[rlim].push_back(o);
    if(p <= n && (o & 1) != (p & 1)){
      addd[q-1].push_back(o);
    }
  }
  for(int i = 1; i <= n; ++ i){
    for(auto o : add[i]){
      if(o & 1){
        t1.add(1, 1, n, o, nxt[o]);
      } else {
        t2.add(1, 1, n, o, nxt[o]);
      }
    }
    for(auto id : rpos[i]){
      int x = t1.ask(1, 1, n, qr[id].l, qr[id].r);
      int y = t2.ask(1, 1, n, qr[id].l, qr[id].r);
      if(x < y){
        mnn[id] = x;
        mnpp[id] = 1;
      } else {
        mnn[id] = y;
        mnpp[id] = 2;
      }
    }
  }
  for(int i = n; i >= 1; -- i){
    for(auto o : addd[i]){
      if(o & 1){
        t3.add(1, 1, n, o, nxt[o]);
      } else {
        t4.add(1, 1, n, o, nxt[o]);
      }
    }
    for(auto id : rpos[i]){
      int x = t3.ask(1, 1, n, qr[id].l, qr[id].r);
      int y = t4.ask(1, 1, n, qr[id].l, qr[id].r);
      apjj[id] = x;
      bpjj[id] = y;
    }
  }
  for(int i = 1; i <= Q; ++ i){
    int x = qr[i].x, y = qr[i].y, l = qr[i].l, r = qr[i].r;
    if(x == y){
      int p = nxx[i];
      if(p <= qr[i].r){
        ans[i] = (p&1) ? 1 : 2;
      }
      continue;
    }
    int mn = mnn[i], mnp = mnpp[i];
    int apj = apjj[i], bpj = bpjj[i];
    int o = l - 2, p = nxx[i], q = nxt[p];
    if(chkwin(o, p, q, r) && p < mn){
      mn = p;
      mnp = o;
    }
    if(chkpj(o, p, q, r)) apj = min(apj, p);
    o = l - 1, p = nxy[i], q = nxt[p];
    if(chkwin(o, p, q, r) && p < mn){
      mn = p;
      mnp = o;
    }
    if(chkpj(o, p, q, r)) bpj = min(bpj, p);
    if(mn >= r + 1) continue;
    if(mnp & 1){
      if(bpj >= mn) ans[i] = 1;
    } else {
      if(apj >= mn) ans[i] = 2;
    }
    if(a[l] == y){
      int p = nxt[l];
      if(p <= r && (p&1)){
        ans[i] = 1;
      }
      if(p > r && ans[i] == 2){
        ans[i] = 0;
      }
    }
  }
  for(int i = 1; i <= Q; ++ i){
    if(ans[i] == 0){
      println_cstr("D");
    } else if(ans[i] == 1){
      println_cstr("A");
    } else {
      println_cstr("B");
    }
  }
  return 0;
}

17. CF840E - In a Trap \(\color{green}\texttt{战胜}\)

https://www.luogu.com.cn/problem/CF840E。
题意:给定一棵树,多次询问一条祖先后代链 \(u\to v\)\(u\)\(v\) 祖先),求 \(\max_{i\in u\to v} a_i\operatorname{xor} dis(i,v)\)\(dis\) 为两点间最短路径经过的边数。
\(n,a_i\leq 50000, q\leq 1.5e5\)

直接树分块感觉不是很能做,考虑值域分块。

对于 \(v->u\) 的链,每 \(256\) 个点分一块,那么每一块的 \(dis\) 的高 \(8\) 位都是相同的。那么考虑还要预处理什么东西:

目前一个块包含 \(v\to anc_{255}v\) 的所有点,每次可以传进一个数 \(x\) 表示 \(dis\) 的高 \(8\) 位,那么需要快速求出与这个 \(x\) 异或起来最大的 \(a_i\) 的高 \(8\) 位。这个可以对于每个块中的 \(a_i\)\(8\) 位插入 trie 中,预处理出来。再考虑低 \(8\) 位,发现 \(a_i\)\(dis\) 的低 \(8\) 位的对应关系是固定的,那么就相当于从一个 \(a_i\) 的子集中选出 \(a_i\operatorname{xor} dis\) 的最大值,对于每个高 \(8\) 位,都可以提前预处理。

那么每次查询一个块的时候,问出高 \(8\) 位的最优解后查这个集合内低 \(8\) 位的最优解即可。

复杂度 \(O(nB\log B + \dfrac{nq}B)\)

点击查看代码
const int N = 5e4 + 10;
int n, q, a[N];
vector<int> g[N];
int dep[N], anc[N], anc256[N];
int mx[N][256], got[N][256];

void dfs(int x, int fa){
  dep[x] = dep[fa] + 1;
  anc[x] = fa;
  for(int i : g[x]){
    if(i == fa){
      continue;
    }
    dfs(i, x);
  }
}

void init(){
  read(n, q);
  READ(a, n);
  for(int i = 1; i < n; ++ i){
    int u, v;
    read(u, v);
    g[u].push_back(v);
    g[v].push_back(u);
  }
}

int cnt[512];

void solve(){
  dep[0] = -1;
  dfs(1, 0);
  for(int i = 1; i <= n; ++ i){
    if(dep[i] >= 256){
      int x = i;
      memset(cnt, 0, sizeof(cnt));
      for(int j = 1; j <= 256; ++ j){
        mx[i][a[x]>>8] = max(mx[i][a[x]>>8], (a[x]&255) ^ (j-1));
        int p = 1;
        ++ cnt[p];
        for(int k = 7; k >= 0; -- k){
          if((a[x]>>8) & (1<<k)){
            p = p << 1 | 1;
          } else {
            p = p << 1;
          }
          ++ cnt[p];
        }
        x = anc[x];
      }
      for(int j = 0; j < 256; ++ j){
        int p = 1, ans = 0;
        for(int k = 7; k >= 0; -- k){
          if(j & (1 << k)){
            if(cnt[p<<1]){
              p <<= 1;
            } else {
              ans |= (1 << k);
              p = p << 1 | 1;
            }
          } else {
            if(cnt[p<<1|1]){
              p = p << 1 | 1;
              ans |= (1 << k);
            } else {
              p <<= 1;
            }
          }
        }
        got[i][j] = ans;
      }
      anc256[i] = x;
    }
  }
  while(q--){
    int u, v;
    read(u, v);
    int edp = dep[v];
    int ans = 0, cc = 0;
    while(dep[v] - dep[u] >= 256){
      int k = got[v][cc];
      ans = max(ans, ((cc^k)<<8) | mx[v][k]);
      v = anc256[v];
      ++ cc;
    }
    while(true){
      ans = max(ans, a[v] ^ (edp - dep[v]));
      if(v == u){
        break;
      }
      v = anc[v];
    }
    println(ans);
  }
}

int main(){
  iobuffinit();
  init();
  int multicases = 1;
  if(MultiCases){
    read(multicases);
  }
  while(multicases --){
    solve();
  }
  return 0;
}

18. three \(\color{red}\texttt{战败}\)

题意:给你 \(n\) 个长度为 \(m\) 的二进制数,\(m\) 位每一位有个贡献 \((a,b)\) 表示当这一位有奇数个 \(1\) 时贡献为 \(a\times 3^b\),否则贡献为 \(0\)。求选择这 \(n\) 个数的一个子集的最大贡献。
\(1\leq n \leq 2e5, 1\leq b\leq m\leq 35,a\in\{-1,1\}\), 保证二元组 \((a,b)\) 互不相同。

\(1\) 的个数为奇数 <-> 异或和为 \(1\)

贡献形式 \(a\times 3^b\) 支持我们逐位贪心。

将所有 \(b\in[1, 35]\) 内没有出现的 \((a,b)\) 补全为全 \(0\) 的一位,那么这些位置不会有任何贡献。把所有数位按照 \(b\) 从小到大排序,那么现在问题转化为从大到小考虑每个 \(b\),要求是某两个数位,其中一个异或和为奇数,另一个为偶数。

考虑线性基。把所有数位按照 \(b\) 从小到大排序,\(b\) 越大的数位越大后,从大到小考虑第 \(i\) 位时线性基的第 \(i\) 位变成了唯一能够改变第 \(i\) 位的数。那么对于一个 \(b\),对应的排序后数位是 \(2b,2b-1\)。分类讨论这两个基长什么样子:

  1. 都存在。那么显然这两位都可以满足条件。
  2. 都不存在。不管。
  3. 只有 \(2b-1\) 存在。第 \(2b\) 位就不管了,第 \(2b-1\) 位可以满足条件。
  4. 只有 \(2b\) 存在,且这个基在 \(2b-1\) 位为 \(0\)。第 \(2b-1\) 位就不管了,第 \(2b\) 位可以满足条件。
  5. 只有 \(2b\) 存在,且这个基在 \(2b-1\) 位为 \(1\)。若这两位都满足或者都不满足,显然是不操作或者操作。但是若这两位其中一位满足另一位不满足,则这个操作是否执行对目前的结果没有影响。那么将这个集删去 \(2b-1,2b\) 两位后再插入回去就可以。
点击查看代码
//three
#include <bits/stdc++.h>
using namespace std;
long long pw3[40], ans;

int c, T, n, m;
char s[200010][77];
int pos[40][2];

typedef bitset<77> B;

struct xxj{
  B bs[77];
  void ins(B x){
    for(int i = m*2; i >= 1; -- i){
      if(!x.test(i)){
        continue;
      }
      if(bs[i].count() == 0){
        bs[i] = x;
        return;
      }
      x ^= bs[i];
    }
  }
} J, nJ;

int main(){
  freopen("three.in", "r", stdin);
  freopen("three.out", "w", stdout);
  pw3[0] = 1;
  for(int i = 1; i <= 35; ++ i){
    pw3[i] = pw3[i-1] * 3;
  }
  scanf("%d%d", &c, &T);
  while(T--){
    ans = 0;
    memset(pos, -1, sizeof(pos));
    for(int i = 0; i < 77; ++ i){
      J.bs[i].reset();
      nJ.bs[i].reset();
    }
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++ i){
      scanf("%s", s[i] + 1);
    }
    for(int i = 1; i <= m; ++ i){
      int a, b;
      scanf("%d%d", &a, &b);
      if(a == -1){
        pos[b][0] = i;
      } else {
        pos[b][1] = i;
      }
    }
    m = 0;
    for(int i = 1; i <= 35; ++ i){
      if(pos[i][0] != -1 || pos[i][1] != -1){
        m = i;
      }
    }
    for(int i = 1; i <= n; ++ i){
      B tmp;
      tmp.reset();
      for(int j = 1; j <= m; ++ j){
        if(pos[j][0] != -1){
          tmp.set(2 * j - 1, s[i][pos[j][0]] - '0');
        }
        if(pos[j][1] != -1){
          tmp.set(2 * j, s[i][pos[j][1]] - '0');
        }
      }
      J.ins(tmp);
    }
    B nw;
    nw.reset();
    for(int i = m; i >= 1; -- i){
      if(J.bs[2*i].count() && J.bs[2*i-1].count()){
        if(!nw.test(2*i)){
          nw ^= J.bs[2*i];
        }
        if(nw.test(2*i-1)){
          nw ^= J.bs[2*i-1];
        }
      } else if(J.bs[2*i-1].count()){
        if(nw.test(2*i-1)){
          nw ^= J.bs[2*i-1];
        }
      } else if(J.bs[2*i].count()){
        if(J.bs[2*i].test(2*i-1)){
          if(!nw.test(2*i) && nw.test(2*i-1)){
            nw ^= J.bs[2*i];
          } else if(!nw.test(2*i) || nw.test(2*i-1)){
            B tmp = J.bs[2*i];
            tmp.set(2*i-1, 0);
            tmp.set(2*i, 0);
            J.ins(tmp);
          }
        } else {
          if(!nw.test(2*i)){
            nw ^= J.bs[2*i];
          }
        }
      }
    }
    for(int i = 1; i <= m; ++ i){
      if(nw.test(2 * i)){
        ans += pw3[i];
      }
      if(nw.test(2 * i - 1)){
        ans -= pw3[i];
      }
    }
    printf("%lld\n", ans);
  }
  return 0;
}

19. fragment \(\color{red}\texttt{战败}\)

题意:有一个 \(n\) 维空间,第 \(i\) 维的值域是 \([1,p_i]\) 的整数,点 \(x\) 能到点 \(y\) 当且仅当 \(x\) 在所有维的坐标都不大于 \(y\) 的。求最小链覆盖。
\(n\leq 32,p\leq 1e9\)

最小链覆盖等价于最长反链。不加证明地给出最长反链为所有坐标和等于 \(M=\dfrac{\sum p_i+1}2\) 的点。

问题转化为将 \(M\) 个球放到 \(n\) 个盒子中,每个盒子放 \([1,p_i]\) 个,求方案数。

考虑容斥,选定 \(S\) 内的盒子都放了 \(>p_i\) 个,则:

\[ans=\sum_{S}(-1)^{|S|}\dbinom{M-sum(S)-1}{n-1} \]

由于 \(n\leq 32\),考虑 meet in the middle。那么右边那个组合数怎么拆?考虑选择的 \(n-1\) 个数分别在哪个部分(即反着用范德蒙德卷积)。拆成 \(\dbinom{M-sum(S_1)-1}{i}\)\(\dbinom{-sum(S_2)}{n-1-i}\) 即可。题解里加了个注意 \(S_1,S_2\) 满足条件当且仅当 \(sum(S_1)+sum(S_2)\leq M-n\),然后双指针。但是我觉得不需要,因为它大于了意味着原组合数 \(n<m\),然后范德蒙德卷积拆一下依旧是 \(0\)

posted @ 2025-04-29 14:46  KiharaTouma  阅读(19)  评论(0)    收藏  举报