2024.02 别急记录

1. WC/CTSC2024 - 水镜 [省选/NOI-]

\(2L=T\),我们可以发现相邻两项之间的大小关系有四种:

  • \(h_i<h_{i+1}\)
  • \(h_i<T-h_{i+1}\)\(h_i+h_{i+1}<T\)
  • \(T-h_i<T-h_{i+1}\)\(h_i>h_{i+1}\)
  • \(T-h_i<h_{i+1}\)\(h_i+h_{i+1}>T\)

那么由 1,2 可得若 \(h_i\geq h_{i+1},h_i+h_{i+1}\geq T\) 则一定会取 \(T-h_i\)

类似地总共四条:

  • \(h_i\geq h_{i+1},h_i+h_{i+1}\geq T\) 则一定会取 \(T-h_i\)
  • \(h_i\geq h_{i+1},h_i+h_{i+1}\leq T\) 则一定会取 \(T-h_{i+1}\)
  • \(h_i\leq h_{i+1},h_i+h_{i+1}\geq T\) 则一定会取 \(h_{i+1}\)
  • \(h_i\leq h_{i+1},h_i+h_{i+1}\leq T\) 则一定会取 \(h_i\)

那么我们接着考虑 \(h_{i-1},h_i,h_{i+1}\) 三者的关系,可得若 \(h_{i-1},h_i\) 满足上述条件 2,\(h_i,h_{i+1}\) 满足上述条件 4,则 \(h_i\) 既要取 \(h_i\) 又要取 \(T-h_i\),所以这个 \(T\) 对于 \(h_i\) 不可行。

那么我们就可以对于每个 \(i\) 求出 \((L_i,R_i)\) 表示区间里有这个 \(i\) 会对 \(T\) 产生的限制:

  • \(h_i\geq\max(h_{i-1},h_{i+1}),T\geq \min(h_{i-1}, h_{i+1}) + h_i\)
  • \(h_i\leq\min(h_{i-1},h_{i+1}),T\leq \max(h_{i-1}, h_{i+1}) + h_i\)

那么一个区间合法当且仅当 \(\max L_i < \min R_i\)。可以枚举左端点并二分右端点求解。

点击查看代码
const int N = 5e5 + 10;
ll lw[20][N], up[20][N], h[N];
int n;

bool chk(int L, int R){
	if(L > R){
		return 1;
	}
	int k = 31 ^ __builtin_clz(R-L+1);
	ll Lm = max(lw[k][L], lw[k][R-(1<<k)+1]);
	ll Rm = min(up[k][L], up[k][R-(1<<k)+1]);
	return Lm < Rm;
}

void solve(){
	read(n);
	for(int i = 1; i <= n; ++ i){
		read(h[i]);
	}
	lw[0][1] = lw[0][n] = -1e18;
	up[0][1] = up[0][n] = 1e18;
	for(int i = 2; i < n; ++ i){
		lw[0][i] = -1e18;
		up[0][i] = 1e18;
		if(h[i-1] <= h[i] && h[i] >= h[i+1]){
			lw[0][i] = min(h[i-1], h[i+1]) + h[i];
		}
		if(h[i-1] >= h[i] && h[i] <= h[i+1]){
			up[0][i] = max(h[i-1], h[i+1]) + h[i];
		}
	}
	for(int i = 1; i < 20; ++ i){
		for(int j = 1; j + (1 << i) - 1 <= n; ++ j){
			lw[i][j] = max(lw[i-1][j], lw[i-1][j+(1<<i-1)]);
			up[i][j] = min(up[i-1][j], up[i-1][j+(1<<i-1)]);
		}
	}
	ll ans = 0;
	for(int i = 1; i <= n; ++ i){
		int l = i, r = n;
		while(l < r){
			int mid = l + r + 1 >> 1;
			if(chk(i + 1, mid - 1)){
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		ans += l - i;
	}
	println(ans);
}

2. CTSC2017 - 吉夫特 [省选/NOI-]

简单题啊。

一个经典结论是 \(\dbinom nm \equiv [n\operatorname{AND} m=m]\pmod 2\)

proof

根据卢卡斯定理有 \(\dbinom nm\equiv \dbinom{n/p}{m/p}\dbinom{n\bmod p}{m\bmod p}\)

然后我们观察到如果 \(n\) 最低位是 \(0\)\(m\) 最低位是 \(1\),那么这个就是 \(0\) 了。

否则这个不会变为 \(0\) 就是 \(1\)

然后就能写出转移方程:\(f_i=\sum_{a_i\operatorname{AND}a_j=a_i}\{f_j+1\}\),使用枚举子集即可 \(O(3^{\log V})\) 转移。

点击查看代码
const int N = 262144 + 10;
const ll P = 1e9 + 7;
int n, a[N], p[N];
ll f[N], ans;

void solve(){
	read(n);
	memset(p, 0x3f, sizeof(p));
	for(int i = 1; i <= n; ++ i){
		read(a[i]);
		p[a[i]] = i;
	}
	for(int i = 0; i <= n; ++ i){
		int s = 262143 - a[i];
		for(int j = s; j > 0; j = (j - 1) & s){
			f[a[i]] = (f[a[i]] + (p[a[i]+j] < i ? f[a[i]+j]+1 : 0)) % P;
		}
		ans = (ans + f[a[i]]) % P;
	}
	println(ans);
}

3. CF1918G - Permutation of Given [*2700]

无聊构造题。

首先 \(n=3,5\) 是无解的。

proof 当 $n=3$ 时,设序列为 $\{a,b,c\}$,变化后为 $\{b,a+c,b\}$,显然二者总和不等,故无解。

\(n=5\) 时,设序列为 \(\{a,b,c,d,e\}\),变化后为 \(\{b,a+c,b+d,c+e,d\}\)\(b=b\)\(d=d\)\(a+c\neq a,c\) 只能 \(=e\)\(c+e=a\),所以 \(b+d=c\);又二者总和相减得 \(b+c+d=0\),所以 \(c=0\),无解。

然后考虑一个合法的长度为 \(n-2\) 的序列,设其最后两项为 \(a,b\),变化后的最后两项为 \(t,a\)

那么若在序列末尾添加 \(x,y\),则 \(\{a,b,x,y\}=\{t,a+x,b+y,x\}\),解得可取 \(x=-b,y=a-b\)

那么我们只用找到 \(n=2\)\(n=7\) 时的解即可。

\(n=2\) 很好找:\(\{1,2\}\)

\(n=7\) 打表可得 \(\{-3,-3,2,1,-1,1,-2\}\)

点击查看代码
const int N = 1e6 + 10;
int n, a[N];

void solve(){
	read(n);
	if(n == 3 || n == 5){
		println_cstr("NO");
	} else if(n & 1){
		println_cstr("YES");
		a[1] = a[2] = -3;
		a[3] = 2;
		a[4] = a[6] = 1;
		a[5] = -1;
		a[7] = -2;
		for(int i = 8; i <= n; i += 2){
			a[i] = - a[i-1];
			a[i+1] = a[i-2] - a[i-1];
		}
		for(int i = 1; i <= n; ++ i){
			printk(a[i]);
		}
	} else {
		println_cstr("YES");
		a[1] = 1;
		a[2] = 2;
		for(int i = 3; i <= n; i += 2){
			a[i] = - a[i-1];
			a[i+1] = a[i-2] - a[i-1];
		}
		for(int i = 1; i <= n; ++ i){
			printk(a[i]);
		}
	}
}

4. COCI2018-2019#4 - Akvizna [NOI/NOI+/CTSC]

wqs 二分+斜率优化。

首先考虑如果没有 \(k\) 的限制该怎么做:倒着做,设 \(f_i\) 表示赢后 \(i\) 个选手的最多奖金,有转移:

\(f_i=\max\{f_j+\dfrac{i-j}j\}=\max\{\dfrac 1i * j + f_j - 1\}\)

可以斜率优化。具体地,用单调队列维护 \((j,f_j-1)\) 的上凸壳,斜线斜率是 \(\dfrac 1i\) 由大到小,故凸壳斜率递减。

然后加上 \(k\) 的限制就是 wqs 二分。二分斜率 \(k_0\),用这条直线去切答案下凸壳,切到的点是 \((k,f_{n,k})\) 即可取答案。二分后的答案应尽量大,所以当可行时 \(mid\to l\)

点击查看代码
const int N = 1e5 + 10;
const double eps = 1e-16;
int n, k, q[N], g[N];
double f[N];

double slp(int x, int y){
	return (f[x] - f[y]) * 1.0 / (x - y);
}

bool chk(double x){
	int l = 1, r = 0;
	q[1] = 0;
	memset(g, 0, sizeof(g));
	for(int i = 1; i <= n; ++ i){
		while(l < r && slp(q[l+1], q[l]) > 1.0 / i + eps){
			++ l;
		}
		f[i] = f[q[l]] + (i - q[l]) * 1.0 / i - x;
		g[i] = g[q[l]] + 1;
		while(l < r && slp(q[r], q[r-1]) + eps < slp(i, q[r])){
			-- r;
		}
		q[++r] = i;
	}
	return g[n] >= k;
}

void solve(){
	scanf("%d%d", &n, &k);
	double l = 0, r = 1e6;
	while(l + eps < r){
		double mid = (l + r) / 2;
		if(chk(mid)){
			l = mid;
		} else {
			r = mid;
		}
	}
	printf("%.9lf\n", f[n] + l * k);
}

5. LuoguP5633 - 最小度限制生成树 [省选/NOI-]

wqs 二分。

考虑给每条连接到 \(s\) 的边 \(-x\),然后二分最小生成树中 \(s\) 度数 \(\geq k\) 的最小 \(x\)

无解情况:\(s\) 本身度数 \(<k\);图不连通;\(x=-inf\) 时度数仍 \(>k\)

点击查看代码
const int N = 1e5 + 10, M = 5e5 + 10;
int n, m, s, k, deg, fa[N], tx, ty;
ll res;
struct edge{
	int u, v, w;
} e[M], E[M];
bool cmp(edge x, edge y){
	return x.w < y.w;
}
int gf(int x){
	return x == fa[x] ? x : fa[x] = gf(fa[x]);
}

int chk(int x){
	for(int i = 1; i <= n; ++ i){
		fa[i] = i;
	}
	ll ans = 0, cnt = 0, i = 1, j = 1;
	while(i <= tx || j <= ty){
		if(i <= tx && (e[i].w < E[j].w - x || j > ty)){
			if(gf(e[i].u) != gf(e[i].v)){
				fa[gf(e[i].u)] = gf(e[i].v);
				ans += e[i].w;
			}
			++ i;
		} else {
			if(gf(E[j].u) != gf(E[j].v)){
				fa[gf(E[j].u)] = gf(E[j].v);
				ans += E[j].w - x;
				++ cnt;
			}
			++ j;
		}
	}
	res = ans;
	return cnt;
}

void solve(){
	read(n, m, s, k);
	for(int i = 1; i <= n; ++ i){
		fa[i] = i;
	}
	for(int i = 1; i <= m; ++ i){
		int u, v, w;
		read(u, v, w);
		fa[gf(u)] = gf(v);
		if(u == s || v == s){
			++ deg;
			E[++ty] = { u, v, w };
		} else {
			e[++tx] = { u, v, w };
		}
	}
	sort(e + 1, e + tx + 1, cmp);
	sort(E + 1, E + ty + 1, cmp);
	int cnt = 0;
	for(int i = 1; i <= n; ++ i){
		if(gf(i) == i){
			++ cnt;
		}
	}
	if(deg < k || cnt > 1 || chk(-1e5) > k){
		println_cstr("Impossible");
		return;
	}
	int l = -1e5, r = 1e5;
	while(l < r){
		int mid = l + r >> 1;
		if(chk(mid) >= k){
			r = mid;
		} else {
			l = mid + 1;
		}
	}
	chk(l);
	println(res + k * l);
}

6. POI2014 - PAN-Solar Panels [提高+/省选-]

显然可以四维数论分块。

但是发现对于 \(\lfloor\dfrac bx\rfloor,\lfloor\dfrac dx\rfloor\) 相同的 \(x\in[l,r]\)\(x=r\) 一定是其中最优的。于是可以二维数论分块解决。

点击查看代码
int T, a, b, c, d;

void solve(){
	read(T);
	while(T--){
		read(a, b, c, d);
		if(b > d){
			swap(a, c);
			swap(b, d);
		}
		int ans = 0;
		for(int l = 1, r; l <= b; l = r + 1){
			r = min(b / (b / l), d / (d / l));
			if((a-1) / r < b / r && (c-1) / r < d / r){
				ans = max(ans, r);
			}
		}
		println(ans);
	}
}

7. Ynoi ER 2021 - TEST_152 [省选/NOI-]

考虑从左往右执行操作的同时使用树状数组记录:第 \(i\) 位记录最后一次操作是操作 \(i\) 的所有位置的值之和。然后把询问离线并绑定到右端点,执行完操作 \(r\) 后询问 \([l,r]\) 的答案即为树状数组的 \([l,r]\) 之和。

接着考虑如何快速操作:使用 set 维护序列连续段及其值、最后修改的时间,然后每次修改找到那些与之有交的序列段进行修改。可以发现每次至多增加两段,故总段数是 \(O(n)\) 的。注意找序列段的实现。

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

点击查看代码
const int N = 5e5 + 10;
int n, m, q, L[N], R[N], v[N];
vector<pair<int, int> > qr[N];
tuple<int, int, int> seq[N];
ll bit[N], ans[N];

void add(int x, ll v){
	++ x;
	while(x <= n + 1){
		bit[x] += v;
		x += x & -x;
	}
}
ll ask(int x){
	++ x;
	ll rs = 0;
	while(x){
		rs += bit[x];
		x -= x & -x;
	}
	return rs;
}
ll ask(int l, int r){
	return ask(r) - ask(l-1);
}

void solve(){
	read(n, m, q);
	for(int i = 1; i <= n; ++ i){
		read(L[i], R[i], v[i]);
	}
	for(int i = 1; i <= q; ++ i){
		int l, r;
		read(l, r);
		qr[r].emplace_back(l, i);
	}
	set<int> st;
	st.insert(1);
	seq[1] = { n, 0, 0 };
	for(int i = 1; i <= n; ++ i){
		while(true){
			auto it = st.upper_bound(R[i]);
			if(it == st.begin()){
				break;
			}
			-- it;
			int l = (*it), r, cl, vl;
			tie(r, cl, vl) = seq[l];
			if(L[i] <= l && r <= R[i]){
				add(cl, -(ll)vl * (r-l+1));
				st.erase(it);
			} else if(L[i] <= l && R[i] < r && l <= R[i]){
				add(cl, -(ll)vl * (R[i]-l+1));
				st.erase(it);
				st.insert(R[i]+1);
				seq[R[i]+1] = { r, cl, vl };
			} else if(l < L[i] && r <= R[i] && L[i] <= r){
				add(cl, -(ll)vl * (r-L[i]+1));
				seq[l] = { L[i]-1, cl, vl };
				break;
			} else if(l < L[i] && R[i] < r){
				add(cl, -(ll)vl * (R[i]-L[i]+1));
				seq[l] = { L[i]-1, cl, vl };
				st.insert(R[i]+1);
				seq[R[i]+1] = {r, cl, vl};
				break;
			} else {
				break;
			}
		}
		st.insert(L[i]);
		seq[L[i]] = { R[i], i, v[i] };
		add(i, (ll)v[i] * (R[i]-L[i]+1));
		for(auto j : qr[i]){
			ans[j.second] = ask(j.first, i);
		}
	}
	for(int i = 1; i <= q; ++ i){
		println(ans[i]);
	}
}

8. CF627F - Island Puzzle [*3400]

思路不难的逆天角盒题!

首先考虑不加边的情况,容易发现将 \(0\) 的位置由起始一步一步移动到目标是唯一的操作方法。那么就先这样进行一下操作,如果已经完成目标就是不用加边的最短步骤;否则一定需要加边。

那么将 \(0\) 放对位置后,还剩下若干 \(a\neq b\) 的位置。我们可以发现 \(0\) 在环上绕圈相当于其他位置进行一个轮换。那么求出这些点的 lca \(p\)。若 \(p\) 与这些点构成一条链,那么就要把链加一条边连成环;否则无解。然后如果环上除掉 \(p\) 以外的点 \(a,b\) 不构成轮换,无解;否则,步骤为 \(0\) 初始位置 \(\to 0\) 目标位置 \(\to p\),然后顺时针或逆时针绕圈,最后返回 \(0\) 目标位置。

但是,有可能有的路径可以省去!比如 \(p\)\(0\) 初始位置 \(\to 0\) 目标位置上,就不用先到 \(0\) 目标位置,而是直接去绕圈。这个对于顺时针和逆时针两种情况分别判一下,因为第一圈的某些步一样可以省去,最后取最小值即可。

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

int st[N], tp;
bool dfs1(int x, int fa){
	st[++tp] = x;
	if(x == b0){
		return 1;
	}
	for(int i : g[x]){
		if(i != fa){
			if(dfs1(i, x)){
				return 1;
			}
		}
	}
	-- tp;
	return 0;
}

int nd[N], dep[N], fat[N], ind[N];
void dfs2(int x, int fa){
	fat[x] = fa;
	dep[x] = dep[fa] + 1;
	for(int i : g[x]){
		if(i != fa){
			dfs2(i, x);
		}
	}
}

int stt[N], tpp, as[N], bs[N], so[N];
bool dfs3(int x, int fa, int gl){
	stt[++tpp] = x;
	if(x == gl){
		return 1;
	}
	for(int i : g[x]){
		if(i != fa && nd[i]){
			if(dfs3(i, x, gl)){
				return 1;
			}
		}
	}
	-- tpp;
	return 0;
}

void solve(){
	read(n);
	for(int i = 1; i <= n; ++ i){
		read(a[i]);
		if(a[i] == 0){
			a0 = i;
		}
	}
	for(int i = 1; i <= n; ++ i){
		read(b[i]);
		if(b[i] == 0){
			b0 = i;
		}
	}
	for(int i = 1; i < n; ++ i){
		int u, v;
		read(u, v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	
	dfs1(a0, 0);
	for(int i = 1; i < tp; ++ i){
		swap(a[st[i]], a[st[i+1]]);
	}
	bool flg = 1;
	for(int i = 1; i <= n; ++ i){
		if(a[i] != b[i]){
			flg = 0;
			break;
		}
	}
	if(flg){
		println(0, tp - 1);
		return;
	}
	
	dfs2(b0, 0);
	int p = 0;
	bool ok = 1;
	for(int i = 1; i <= n; ++ i){
		if(a[i] != b[i]){
			nd[i] = 1;
		}
	}
	for(int i = 1; i <= n; ++ i){
		if(nd[i]){
			if(!p || dep[p] >= dep[i]){
				p = fat[i];
			}
		}
	}
	
	nd[p] = 1;
	for(int i = 1; i <= n; ++ i){
		for(int j : g[i]){
			if(i < j && nd[i] && nd[j]){
				++ ind[i];
				++ ind[j];
			}
		}
	}
	int c = 0;
	for(int i = 1; i <= n; ++ i){
		if(ind[i] == 1){
			++ c;
		} else if(nd[i] && ind[i] != 2){
			ok = 0;
			break;
		}
	}
	if(c != 2){
		ok = 0;
	}
	if(!ok){
		println(-1);
		return;
	}
	
	int u = 0, v = 0;
	for(int i = 1; i <= n; ++ i){
		if(ind[i] == 1){
			if(u){
				v = i;
			} else {
				u = i;
			}
		}
	}
	dfs3(u, 0, v);
	int psp = 0;
	for(int i = 1, q = 0; i <= tpp; ++ i){
		if(stt[i] != p){
			as[++q] = a[stt[i]];
			bs[q] = b[stt[i]];
		} else {
			psp = i;
		}
	}
	int dy = 0;
	for(int i = 1; i < tpp; ++ i){
		if(as[1] == bs[i]){
			dy = i;
			for(int j = 1, k = i; j < tpp; ++ j, ++ k){
				if(k == tpp){
					k = 1;
				}
				if(as[j] != bs[k]){
					ok = 0;
					break;
				}
			}
			break;
		}
	}
	if(dy == 0){
		ok = 0;
	}
	if(!ok){
		println(-1);
		return;
	}
	
	int tmp = p;
	while(tmp != b0){
		so[fat[tmp]] = tmp;
		tmp = fat[tmp];
	}
	ll ac = 2, rc = 2;
	ac += tp - 1 + (dep[p] - 1) * 2;
	rc += tp - 1 + (dep[p] - 1) * 2;
	ac += (dy - 1) * 1ll * tpp;
	rc += (tpp - dy) * 1ll * tpp;
	
	int np = b0, nq = b0, fl = tp, pr = 0;
	while(true){
		if(np == nq){
			-- rc;
			-- rc;
		}
		if(np == a0 || nq == stt[tpp]){
			break;
		}
		-- fl;
		np = st[fl];
		if(nq == p){
			pr = psp;
		}
		if(!pr){
			nq = so[nq];
		} else {
			++ pr;
			nq = stt[pr];
		}
	}
	np = b0, nq = b0, fl = tp, pr = 0;
	while(true){
		if(np == nq){
			-- ac;
			-- ac;
		}
		if(np == a0 || nq == stt[1]){
			break;
		}
		-- fl;
		np = st[fl];
		if(nq == p){
			pr = psp;
		}
		if(!pr){
			nq = so[nq];
		} else {
			-- pr;
			nq = stt[pr];
		}
	}
	
	println(u, v, min(ac, rc));
}

9. COCI2021-2022#2 - Osumnjičeni [省选/NOI-]

双指针+线段树可以求出对于每个人,最右边能选到哪个人。然后倍增即可。

点击查看代码
const int N = 2e5 + 10, M = 4e5 + 10;
int n, q, l[N], r[N], pl[M], tp, t[M*4], tag[M*4], f[N][20];

void psd(int p){
	if(tag[p] == -1){
		return;
	}
	t[p<<1] = t[p<<1|1] = tag[p<<1] = tag[p<<1|1] = exchange(tag[p], -1);
}
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] = tag[p] = v;
	} else {
		int mid = l + r >> 1;
		psd(p);
		add(p<<1, l, mid, ql, qr, v);
		add(p<<1|1, mid+1, r, ql, qr, v);
		t[p] = max(t[p<<1], t[p<<1|1]);
	}
}
int qry(int p, int l, int r, int ql, int qr){
	if(qr < l || r < ql){
		return 0;
	} else if(ql <= l && r <= qr){
		return t[p];
	} else {
		int mid = l + r >> 1;
		psd(p);
		return max(qry(p<<1, l, mid, ql, qr), qry(p<<1|1, mid+1, r, ql, qr));
	}
}

void solve(){
	read(n);
	memset(tag, -1, sizeof(tag));
	for(int i = 1; i <= n; ++ i){
		read(l[i], r[i]);
		pl[++tp] = l[i];
		pl[++tp] = r[i];
	}
	sort(pl + 1, pl + tp + 1);
	tp = unique(pl + 1, pl + tp + 1) - pl - 1;
	for(int i = 1; i <= n; ++ i){
		l[i] = lower_bound(pl + 1, pl + tp + 1, l[i]) - pl;
		r[i] = lower_bound(pl + 1, pl + tp + 1, r[i]) - pl;
	}
	int L = 1, R = 0;
	while(R < n && qry(1, 1, tp, l[R+1], r[R+1]) == 0){
		add(1, 1, tp, l[R+1], r[R+1], 1);
		++ R;
	}
	f[1][0] = R + 1;
	for(L = 2; L <= n; ++ L){
		add(1, 1, tp, l[L-1], r[L-1], 0);
		while(R < n && qry(1, 1, tp, l[R+1], r[R+1]) == 0){
			add(1, 1, tp, l[R+1], r[R+1], 1);
			++ R;
		}
		f[L][0] = R + 1;
	}
	f[n+1][0] = n + 1;
	for(int i = 1; i < 20; ++ i){
		for(int j = 1; j <= n + 1; ++ j){
			f[j][i] = f[f[j][i-1]][i-1];
		}
	}
	read(q);
	while(q--){
		int x, y, ans = 0;
		read(x, y);
		for(int i = 19; i >= 0; -- i){
			if(f[x][i] <= y){
				ans += (1 << i);
				x = f[x][i];
			}
		}
		println(ans + 1);
	}
}

10. BJOI2014 - 想法 [省选/NOI-]

非常厉害!

发现直接做不好做。我们考虑给每个 \([1,m]\) 之间的点附一个随机权值,然后求每个点所能到达的最小权值,设其为 \(s\),则 \(s\) 的期望为 \(\dfrac{RAND\_MAX}{k+1}\),其中 \(k\) 为所能到达点的数量。那么我们就可以多跑几次来估计出 \(k\)

点击查看代码
const int N = 2e6 + 10, T = 100;
int c[N][2], f[N][T], n, m;
double ans[N];

void solve(){
	read(n, m);
	for(int i = m + 1; i <= n; ++ i){
		read(c[i][0], c[i][1]);
	}
	for(int p = 0; p <= 2; ++ p){
		for(int i = 1; i <= m; ++ i){
			for(int j = 0; j < T; ++ j){
				f[i][j] = rand();
			}
		}
		for(int i = m + 1; i <= n; ++ i){
			for(int j = 0; j < T; ++ j){
				f[i][j] = min(f[c[i][0]][j], f[c[i][1]][j]);
				ans[i] += f[i][j];
			}
		}
	}
	for(int i = m + 1; i <= n; ++ i){
		println((int)(RAND_MAX / ans[i] * T * 3 - 0.5));
	}
}

11. CF809E - Surprise me! [*3100]

虚树+推式子。

\[\frac{1}{n(n-1)}\sum_{i=1}^n\sum_{j\ne i}\varphi(a_i*a_j)* dist(i,j) \]

我们有 \(\varphi(xy) = \dfrac{\varphi(x)\varphi(y)(x,y)}{\varphi((x,y))}\)

proof

\(\varphi(n)\varphi(m)=n\prod_{i|n}(\frac{i-1}i)m\prod_{j|m}(\frac{j-1}j)\)

稍微变形,

\(=nm\prod_{i|nm}(\frac{i-1}i)\prod_{j|(n,m)}(\frac{j-1}j)=\varphi(nm)\frac{\gcd(n,m)}{\varphi(\gcd(n,m))}\)

所以原式可以变为:

\[\sum_{i=1}^n\sum_{j\ne i}\varphi(a_i)\varphi(a_j)\dfrac{\gcd(a_i,a_j)}{\varphi(\gcd(a_i,a_j))}* \operatorname{dist}(i,j) \]

枚举 \(\gcd\)

\[\sum_{d=1}^n \dfrac{d}{\varphi(d)}\sum_{i=1}^n\sum_{j\ne i}\varphi(a_i)\varphi(a_j)[\gcd(a_i,a_j)=d]\operatorname{dist}(i,j) \]

设:

\[f(d)=\sum_{i=1}^n\sum_{j\ne i}\varphi(a_i)\varphi(a_j)[\gcd(a_i,a_j)=d]\operatorname{dist}(i,j) \]

等于不好算,换成倍数,设:

\[F(d)=\sum_{i=1}^n\sum_{j\ne i}\varphi(a_i)\varphi(a_j)[d|a_i,d|a_j]\operatorname{dist}(i,j) \]

反演:

\[f(d)=\sum_{d|x} F(x)\mu(\frac xd) \]

考虑计算 \(F(d)\)。把树上所有 \(d|a_i\) 的点拎出来建虚树,消掉中括号。有 \(\operatorname{dist}(i,j)=dep_i+dep_j-2dep_{\operatorname{lca}(i,j)}\),所以:

\[F(d)=\sum_{i\in V_{vt}}\sum_{j\ne i}\varphi(a_i)\varphi(a_j)(dep_i+dep_j-2dep_{\operatorname{lca}(i,j)}) \]

变形,得:

\[F(d)=2\sum_i\varphi(a_i)dep_i\sum_j\varphi(a_j)-2\sum_i\sum_j\varphi(a_i)\varphi(a_j)dep_{\operatorname{lca}(i,j)} \]

前面的直接处理,后面的可以 dp 的同时在 \(lca\) 处统计答案。

点击查看代码
const int N = 4e5 + 10;
const ll P = 1e9 + 7;
int n, a[N], v[N], ps[N];
ll F[N], f[N];

namespace Sieve{
	int p[N], v[N], c, phi[N], mu[N];
	void clc(int n){
		phi[1] = mu[1] = 1;
		for(int i = 2; i <= n; ++ i){
			if(!v[i]){
				p[++c] = i;
				phi[i] = i - 1;
				mu[i] = P - 1;
			}
			for(int j = 1; j <= c && i * p[j] <= n; ++ j){
				v[i*p[j]] = 1;
				if(i % p[j]){
					phi[i*p[j]] = phi[i] * phi[p[j]];
					mu[i*p[j]] = P - mu[i];
				} else {
					phi[i*p[j]] = phi[i] * p[j];
					mu[i*p[j]] = 0;
					break;
				}
			}
		}
	}
	ll inv(ll x){
		ll ans = 1, y = P - 2;
		while(y){
			if(y & 1){
				ans = ans * x % P;
			}
			x = x * x % P;
			y >>= 1;
		}
		return ans;
	}
}

namespace VirtualTree{
	int dfn[N], dfc, dep[N], st[20][N];
	vector<int> g[N];
	int get(int x, int y){
		return dfn[x] < dfn[y] ? x : y;
	}
	void dfs(int x, int fa){
		dfn[x] = ++ dfc;
		st[0][dfn[x]] = fa;
		dep[x] = dep[fa] + 1;
		for(int i : g[x]){
			if(i != fa){
				dfs(i, x);
			}
		}
	}
	void init(){
		dfs(1, 0);
		for(int i = 1; i < 20; ++ i){
			for(int j = 1; j + (1<<i) - 1 <= n; ++ j){
				st[i][j] = get(st[i-1][j], st[i-1][j+(1<<i-1)]);
			}
		}
	}
	int lca(int u, int v){
		if(u == v){
			return u;
		}
		if((u = dfn[u]) > (v = dfn[v])){
			swap(u, v);
		}
		int d = 31 ^ __builtin_clz(v - u ++);
		return get(st[d][u], st[d][v-(1<<d)+1]);
	}
	int h[N], m, a[N], len, val[N];
	vector<pair<int, int> > G[N];
	set<int> s;
	int DEP[N];
	bool cmp(int x, int y){
		return dfn[x] < dfn[y];
	}
	void bld(){
		h[++m] = 1;
		sort(h + 1, h + m + 1, cmp);
		len = 0;
		bool is1 = 0;
		for(int i = 1; i < m; ++ i){
			a[++len] = h[i];
			a[++len] = lca(h[i], h[i+1]);
		}
		for(int i = 2; i <= m; ++ i){
			val[h[i]] = v[h[i]];
		}
		a[++len] = h[m];
		sort(a + 1, a + len + 1, cmp);
		len = unique(a + 1, a + len + 1) - a - 1;
		for(int i = 1; i < len; ++ i){
			int lc = lca(a[i], a[i+1]);
			G[lc].emplace_back(a[i+1], dep[a[i+1]] - dep[lc]);
			s.insert(lc);
			s.insert(a[i]);
		}
		s.insert(a[len]);
	}
	ll siz[N], x, y, z;
	void clr(){
		for(int i : s){
			val[i] = siz[i] = DEP[i] = 0;
			vector<pair<int, int> > ().swap(G[i]);
		}
		s.clear();
		m = len = 0;
	}
	
	void dfss(int x, int fa){
		for(auto i : G[x]){
			int y = i.first;
			DEP[y] = DEP[x] + i.second;
			dfss(y, x);
			z = (z + siz[x] * siz[y] % P * DEP[x]) % P;
			siz[x] += siz[y];
		}
		z = z + (siz[x] * val[x] % P * DEP[x]) % P;
		siz[x] = (siz[x] + val[x]) % P;
	}
	
	ll solve(){
		bld();
		x = 0, y = 0, z = 0;
		dfss(1, 0);
		z = z * 2 % P;
		for(int i = 1; i <= len; ++ i){
			x = (x + (ll)val[a[i]] * DEP[a[i]]) % P;
			y = (y + val[a[i]]) % P;
			z = (z + (ll)val[a[i]] * val[a[i]] % P * DEP[a[i]]) % P;
		}
		clr();
		return ((2 * x * y - 2 * z) % P + P) % P;
	}
}

void solve(){
	read(n);
	Sieve::clc(n);
	for(int i = 1; i <= n; ++ i){
		read(a[i]);
		v[i] = Sieve::phi[a[i]];
		ps[a[i]] = i;
	}
	for(int i = 1; i < n; ++ i){
		int u, v;
		read(u, v);
		VirtualTree::g[u].push_back(v);
		VirtualTree::g[v].push_back(u);
	}
	VirtualTree::init();
	for(int x = 1; x <= n; ++ x){
		VirtualTree::m = 0;
		for(int i = x; i <= n; i += x){
			VirtualTree::h[++VirtualTree::m] = ps[i];
		}
		F[x] = VirtualTree::solve();
	}
	for(int x = 1; x <= n; ++ x){
		for(int d = x; d <= n; d += x){
			f[x] = (f[x] + F[d] * Sieve::mu[d/x]) % P;
		}
	}
	ll ans = 0;
	for(int d = 1; d <= n; ++ d){
		ans = (ans + d * Sieve::inv(Sieve::phi[d]) % P * f[d]) % P;
	}
	println(ans * Sieve::inv((ll)n * (n-1) % P) % P);
}

12. NEERC2017 - Journey from Petersburg to Moscow [省选/NOI-]

发现 \(n,m\leq 3000\),考虑枚举第 \(k\) 大边权值 \(W\)

对于枚举的 \(W\),令每条边权值 \(w'=\max(0,w-W')\),然后跑最短路,用 \(\operatorname{MinDist}(1,n)+kW\) 更新答案。

考虑为什么是对的:

  • 对于第 \(k\) 大权值正好是 \(W\) 的路径,显然最后算到的答案相同;
  • 对于第 \(k\) 大权值大于 \(W\) 的路径,答案会多加上 \([W+1,w']\) 这一部分边的权值与 \(W\) 差值的和,更大;
  • 对于第 \(k\) 大权值小于 \(W\) 的路径,答案会多加上 \([w'+1,W]\) 这一部分边的权值与 \(W\) 差值的和,更大。

故一定能找到最小解。

点击查看代码
#define int long long
const int N = 3010;
int n, m, k, pl[N], vs[N], U[N], V[N], W[N];
vector<pair<int, int> > g[N];
ll ds[N];

ll dij(){
	priority_queue<pair<int, int> > q;
	memset(ds, 0x3f, sizeof(ds));
	memset(vs, 0, sizeof(vs));
	ds[1] = 0;
	q.push(make_pair(0, 1));
	while(!q.empty()){
		int x = q.top().second;
		q.pop();
		if(vs[x]){
			continue;
		}
		vs[x] = 1;
		for(auto i : g[x]){
			int y = i.first, z = i.second;
			if(ds[y] > ds[x] + z){
				ds[y] = ds[x] + z;
				q.push(make_pair(-ds[y], y));
			}
		}
	}
	return ds[n];
}

void solve(){
	read(n, m, k);
	for(int i = 1; i <= m; ++ i){
		read(U[i], V[i], W[i]);
		pl[i] = W[i];
	}
	sort(pl + 1, pl + m + 1);
	int tp = unique(pl + 1, pl + m + 1) - pl - 1;
	ll ans = 1e18;
	for(int i = 0; i <= tp; ++ i){
		for(int j = 1; j <= m; ++ j){
			g[U[j]].emplace_back(V[j], max(0ll, W[j] - pl[i]));
			g[V[j]].emplace_back(U[j], max(0ll, W[j] - pl[i]));
		}
		ans = min(ans, dij() + (ll)k * pl[i]);
		for(int j = 1; j <= n; ++ j){
			vector<pair<int, int> > ().swap(g[j]);
		}
	}
	println(ans);
}

13. CF575A - Fibonotci [*2700]

直接矩阵快速幂就行了。

点击查看代码
const int N = 5e4 + 10;
int n, m;
ll s[N], P, k;
pair<ll, ll> q[N];
map<ll, ll> mp;

struct mat{
	ll a, b, c, d;
} t[N*4];
mat operator * (mat x, mat y){
	mat z;
	z.a = (x.a * y.a + x.b * y.c) % P;
	z.b = (x.a * y.b + x.b * y.d) % P;
	z.c = (x.c * y.a + x.d * y.c) % P;
	z.d = (x.c * y.b + x.d * y.d) % P;
	return z;
}

void bld(int p, int l, int r){
	if(l == r){
		t[p] = { 0, s[(l+n-1)%n], 1, s[l%n] };
	} else {
		int mid = l + r >> 1;
		bld(p<<1, l, mid);
		bld(p<<1|1, mid+1, r);
		t[p] = t[p<<1] * t[p<<1|1];
	}
}
mat qry(int p, int l, int r, int ql, int qr){
	if(qr < l || r < ql){
		return { 1, 0, 0, 1 };
	} else if(ql <= l && r <= qr){
		return t[p];
	} else {
		int mid = l + r >> 1;
		return qry(p<<1, l, mid, ql, qr) * qry(p<<1|1, mid+1, r, ql, qr);
	}
}
mat qp(mat x, ll y){
	mat ans = x;
	-- y;
	while(y){
		if(y & 1){
			ans = ans * x;
		}
		x = x * x;
		y >>= 1;
	}
	return ans;
}

void cg(mat &nw, ll l, ll r){
	if(l > r){
		return;
	}
	if(mp[l-1]){
		nw = nw * (mat){ 0, mp[l-1], 1, s[l%n] };
		++ l;
	}
	if(l > r){
		return;
	}
	if(l / n == r / n){
		nw = nw * qry(1, 0, n-1, l%n, r%n);
	} else {
		nw = nw * qry(1, 0, n-1, l%n, n-1);
		ll L = (l / n + 1) * n, R = (r / n) * n;
		if(L != R) nw = nw * qp(qry(1, 0, n-1, 0, n-1), (R-L) / n);
		nw = nw * qry(1, 0, n-1, 0, r%n);
	}
}

void solve(){
	scanf("%lld%lld%d", &k, &P, &n);
	if(k == 0){
		puts("0");
		return;
	}
	for(int i = 0; i < n; ++ i){
		scanf("%lld", &s[i]);
	}
	scanf("%d", &m);
	int tp = 0;
	for(int i = 1; i <= m; ++ i){
		ll x, y;
		scanf("%lld%lld", &x, &y);
		if(x < k){
			q[++tp] = make_pair(x, y);
			mp[x] = y;
		}
	}
	m = tp;
	sort(q + 1, q + m + 1);
	bld(1, 0, n-1);
	mat nw = { 1, 0, 0, 1 };
	for(int i = 1; i < n; ++ i){
		nw = nw * qry(1, 0, n-1, i%n, i%n);
		if(i == k - 1){
			printf("%lld\n", nw.d);
			return;
		}
	}
	ll l = n, r, vl;
	for(int i = 1; i <= m; ++ i){
		r = q[i].first, vl = q[i].second;
		if(r == q[i-1].first) continue;
		cg(nw, l, r-1);
		if(mp[r-1]){
			nw = nw * (mat){ 0, mp[r-1], 1, vl };
		} else {
			nw = nw * (mat){ 0, s[(r-1)%n], 1, vl };
		}
		l = r + 1;
	}
	cg(nw, l, k-1);
	printf("%lld\n", nw.d % P);
	return;
}

14. CF888G - Xor-MST [*2300]

性质图 MST 考虑 boruvka。问题转化为如何求集合去掉一个子集剩下部分与 \(x\) 的最小异或和。可以使用 trie 来维护全集,查询的时候暴力删掉子集里的元素即可。易得复杂度是正确的。

点击查看代码
const int N = 2e5 + 10;
int n, a[N], fa[N];
int ch[N*34][2], siz[N*34], tot, ed[N*34];
vector<int> son[N];

void ins(int x, int vl){
	int p = 0;
	for(int i = 30; i >= 0; -- i){
		if(!ch[p][(a[x]>>i)&1]){
			ch[p][(a[x]>>i)&1] = ++ tot;
		}
		p = ch[p][(a[x]>>i)&1];
		siz[p] += vl;
	}
	ed[p] = x;
}
int fnd(int x){
	int p = 0;
	for(int i = 30; i >= 0; -- i){
		if(siz[ch[p][(x>>i)&1]]){
			p = ch[p][(x>>i)&1];
		} else {
			p = ch[p][1-((x>>i)&1)];
		}
	}
	return ed[p];
}

int gf(int x){
	return x == fa[x] ? x : gf(fa[x]);
}
void mg(int x, int y){
	x = gf(x);
	y = gf(y);
	if(son[x].size() < son[y].size()){
		swap(x, y);
	}
	fa[y] = x;
	for(int i : son[y]){
		son[x].push_back(i);
	}
}

void solve(){
	read(n);
	for(int i = 1; i <= n; ++ i){
		read(a[i]);
	}
	sort(a + 1, a + n + 1);
	n = unique(a + 1, a + n + 1) - a - 1;
	for(int i = 1; i <= n; ++ i){
		fa[i] = i;
		ins(i, 1);
		son[i].push_back(i);
	}
	int cnt = n;
	ll ans = 0;
	while(cnt > 1){
		static int gx[N], gy[N];
		for(int j = 1; j <= n; ++ j){	
			if(j == gf(j)){
				int vl = 2e9;
				for(int k : son[j]){
					ins(k, -1);
				}
				for(int k : son[j]){
					int p = fnd(a[k]);
					if(vl > (a[p] ^ a[k])){
						vl = a[p] ^ a[k];
						gx[j] = k;
						gy[j] = p;
					}
				}
				for(int k : son[j]){
					ins(k, 1);
				}
			}
		}
		for(int j = 1; j <= n; ++ j){
			if(gf(j) == j && gf(gx[j]) != gf(gy[j])){
				mg(gx[j], gy[j]);
				ans += a[gx[j]] ^ a[gy[j]];
				-- cnt;
			}
		}
	}
	println(ans);
}

15. Dynamic Rankings [省选/NOI-]

树状数组套线段树。

树状数组每个节点维护的是一段区间的值,那么我们可以用线段树来维护这段区间。单点加就在树状数组上若干点对应线段树加,区间查就提取出区间,在维护这些区间的线段树上一起查。

点击查看代码
const int N = 2e5 + 10, M = 4e7 + 10;
int n, m, a[N], b[N], tp, rt[N];
struct qry{
	int op, l, r, k;
} q[N];
int t[M], cnt, ls[M], rs[M];
vector<int> ts, qs;

void add(int &p, int l, int r, int x, int v){
	++ cnt;
	t[cnt] = t[p];
	ls[cnt] = ls[p];
	rs[cnt] = rs[p]; 
	p = cnt;
	if(l == r){
		t[p] += v;
	} else {
		int mid = l + r >> 1;
		if(x <= mid){
			add(ls[p], l, mid, x, v);
		} else {
			add(rs[p], mid+1, r, x, v);
		}
		t[p] = t[ls[p]] + t[rs[p]];
	}
}
int qry(int l, int r, int k){
	if(l == r){
		return l;
	}
	int mid = l + r >> 1, x = 0, p = ts.size();
	for(int i = 0; i < p; ++ i){
		x += qs[i] * t[ls[ts[i]]];
	}
	if(x >= k){
		for(int i = 0; i < p; ++ i){
			ts[i] = ls[ts[i]];
		}
		return qry(l, mid, k);
	} else {
		for(int i = 0; i < p; ++ i){
			ts[i] = rs[ts[i]];
		}
		return qry(mid+1, r, k-x);
	}
}

void Add(int x, int v, int op){
	while(x <= n){
		add(rt[x], 1, tp, v, op);
		x += x & -x;
	}
}
int Qry(int l, int r, int k){
	vector<int> ().swap(ts);
	vector<int> ().swap(qs);
	while(r){
		ts.push_back(rt[r]);
		qs.push_back(1);
		r -= r & -r;
	}
	-- l;
	while(l){
		ts.push_back(rt[l]);
		qs.push_back(-1);
		l -= l & -l;
	}
	return qry(1, tp, k);
}

int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; ++ i){
		scanf("%d", &a[i]);
		b[i] = a[i];
	}
	tp = n;
	for(int i = 1; i <= m; ++ i){
		char s[4];
		scanf("%s", s);
		if(s[0] == 'Q'){
			q[i].op = 0;
			scanf("%d%d%d", &q[i].l, &q[i].r, &q[i].k);
		} else {
			q[i].op = 1;
			scanf("%d%d", &q[i].l, &q[i].r);
			b[++tp] = q[i].r;
		}
	}
	sort(b + 1, b + tp + 1);
	tp = unique(b + 1, b + tp + 1) - b - 1;
	for(int i = 1; i <= n; ++ i){
		a[i] = lower_bound(b + 1, b + tp + 1, a[i]) - b;
		Add(i, a[i], 1);
	}
	for(int i = 1; i <= m; ++ i){
		if(q[i].op == 1){
			Add(q[i].l, a[q[i].l], -1);
			a[q[i].l] = lower_bound(b + 1, b + tp + 1, q[i].r) - b;
			Add(q[i].l, a[q[i].l], 1);
		} else {
			printf("%d\n", b[Qry(q[i].l, q[i].r, q[i].k)]);
		}
	}
	return 0;
}

16. EC Online 2023 (I) - Alice and Bob

考场策略肯定是打表找规律,可以发现如下规律:

三元组 \((i,j,k)(i\leq j\leq k)\) 必败,当且仅当 \(i==j\)\(k-j\) 的质因数分解中有偶数个 \(2\)

知道这个结论后就很好做了。把每个数倒着插入 trie 中即可。

点击查看代码
int T, n, cnt[N], ps[N];
ll a[N], b[N];

inline ll clc(ll x){
    return x * (x-1) * (x-2) / 6;
}

int ch[N*64][2], siz[N*64], tt;
inline void ins(ll x){
    int p = 0;
    ++ siz[p];
    for(ll i = 0; i <= 62; ++ i){
        if(!ch[p][(x>>i)&1]){
            ch[p][(x>>i)&1] = ++ tt;
        }
        p = ch[p][(x>>i)&1];
        ++ siz[p];
    }
}
inline void qry(ll x, ll &ans){
    int p = 0, op = 1;
    ans += siz[p] * op;
    op *= -1;
    for(ll i = 0; i <= 62; ++ i){
        p = ch[p][(x>>i)&1];
        ans += siz[p] * op;
        op *= -1;
    }
}

int main(){
    scanf("%d", &T);
    while(T--){
        scanf("%d", &n);
        ll ans = clc(n);
        for(int i = 1; i <= n; ++ i){
            scanf("%lld", &a[i]);
            b[i] = a[i];
            ins(a[i]);
        }
        sort(b + 1, b + n + 1);
        int m = unique(b + 1, b + n + 1) - b - 1;
        for(register int i = 1; i <= n; ++ i){
            ps[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
            ++ cnt[ps[i]];
        }
        for(register int i = 1; i <= n; ++ i){
            int p = ps[i];
            if(cnt[p]){
                ll cc = cnt[p], dd = 0;
                cnt[p] = 0;
                qry(a[i], dd);
                ans -= dd * cc * (cc-1) / 2 + clc(cc);
            }
        }
        printf("%lld\n", ans);
        for(register int i = 0; i <= tt; ++ i){
            ch[i][0] = ch[i][1] = siz[i] = 0;
        }
        for(register int i = 0; i <= n; ++ i){
            cnt[i] = 0;
        }
        tt = 0;
    }
    return 0;
}

17. CF1615F - LEGOndary Grandmaster [*2800]

将两个串的奇数位取反,发现操作变为了交换相邻位的值。那么我们就可以 dp:设 \(f_{i,j}\) 表示前 \(i\) 位,第一个串还有 \(j\) 位没有匹配的方案数;\(g_{i,j}\) 表示同样状态的贡献和。\(f\) 的转移显然,\(g\) 的转移:\(g_{i,k}=f_{i-1,j}*|j|+g_{i-1,j}\)

点击查看代码
const int N = 2010;
int T, n;
char s[N], t[N];
const ll P = 1e9 + 7;
ll f[N][N*2], g[N][N*2];

int main(){
	scanf("%d", &T);
	while(T--){
		scanf("%d%s%s", &n, s + 1, t + 1);
		for(int i = 1; i <= n; ++ i){
			memset(f[i], 0, sizeof(f[i]));
			memset(g[i], 0, sizeof(g[i]));
			if(s[i] != '?' && (i & 1)){
				s[i] = '0' + '1' - s[i];
			}
			if(t[i] != '?' && (i & 1)){
				t[i] = '0' + '1' - t[i];
			}
		}
		f[0][n] = 1;
		for(int i = 1; i <= n; ++ i){
			for(int j = 0; j <= n+n; ++ j){
				for(int k = 0; k < 2; ++ k){
					for(int l = 0; l < 2; ++ l){
						if(s[i] != '?' && s[i] != k + '0'){
							continue;
						}
						if(t[i] != '?' && t[i] != l + '0'){
							continue;
						}
						if(j+k-l < 0){
							continue;
						}
						f[i][j+k-l] = (f[i][j+k-l] + f[i-1][j]) % P;
						g[i][j+k-l] = (g[i][j+k-l] + g[i-1][j] + f[i-1][j] * abs(j-n)) % P;
					}
				}
			}
		}
		printf("%lld\n", g[n][n]);
		f[0][n] = 0;
	}
	return 0;
}

18. 湖北省选模拟 2023 - 棋圣 / alphago [省选/NOI-]

考虑答案上界:

  • 若原图不是二分图,那么每两个棋子之间都可能距离 \(1\)。上界是 \(cnt_0\times cnt_1\times \max_{(u,v)\in E}w_{u,v}\)
  • 否则,只有染色不同的两个棋子之间可能距离 \(1\)。上界是 \((cnt_{0,1}\times cnt_{1,0} + cnt{0,0}\times cnt_{1,1}) \max_{(u,v)\in E}w_{u,v}\)

可以证明只有原图是链的情况达不到上界。

proof

若图中存在环,首先可以将所有棋子移动到环上。环上的所有棋子都可以任意选择一个方向移动一步,就可以将所有棋子聚在一起。

若不存在环且不是链,则存在一个度数 \(\geq 3\) 的点。反证,若存在三个棋子使得其中任意两个永远不可能在同一个位置,则我们可以将他们移动到这个点以及它的两个邻点上,接着再次操作一个没有棋子的邻点,使得其中两个在同一个位置,矛盾。

最后是链的情况,发现两个棋子可行的距离与原距离同奇偶且不大于原距离。于是可以设 \(f_{i,j,k}\) 表示 \([i,j]\) 的棋子在第 \(k\) 个点上的最大答案,dp 即可。

点击查看代码
const int N = 110;
int n, m, k, cl[N], mx, mxdeg;
pair<int, int> ch[N];
vector<pair<int, int> > g[N];

void dfs(int x, int c){
	cl[x] = c;
	for(auto i : g[x]){
		if(cl[i.first] == -1){
			dfs(i.first, 1-c);
		}
	}
}

int main(){
	scanf("%d%d%d", &n, &m, &k);
	for(int i = 1; i <= k; ++ i){
		scanf("%d%d", &ch[i].first, &ch[i].second);
	}
	for(int i = 1; i <= m; ++ i){
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		mx = max(mx, w);
		g[u].emplace_back(v, w);
		g[v].emplace_back(u, w);
	}
	for(int i = 1; i <= n; ++ i){
		mxdeg = max(mxdeg, (int)g[i].size());
	}
	memset(cl, -1, sizeof(cl));
	dfs(1, 0);
	if(mxdeg > 2 || m >= n){
		bool flg = 1;
		for(int i = 1; i <= n; ++ i){
			for(auto j : g[i]){
				if(cl[i] == cl[j.first]){
					flg = 0;
					break;
				}
			}
		}
		if(flg){
			ll x = 0, xx = 0, y = 0, yy = 0;
			for(int i = 1; i <= k; ++ i){
				if(ch[i].second == 0){
					if(cl[ch[i].first]){
						++ x;
					} else {
						++ xx;
					}
				} else {
					if(cl[ch[i].first]){
						++ y;
					} else {
						++ yy;
					}
				}
			}
			printf("%lld\n", (x * yy + xx * y) * mx);
		} else {
			ll x = 0, y = 0;
			for(int i = 1; i <= k; ++ i){
				if(ch[i].second == 0){
					++ x;
				} else {
					++ y;
				}
			}
			printf("%lld\n", x * y * mx);
		}
	} else {
		static int nid[N], w[N], sum[N][2], ok[N][N];
		memset(ok, 0, sizeof(ok));
		memset(sum, 0, sizeof(sum));
		int x = 0, ls = 0;
		for(int i = 1; i <= n; ++ i){
			if(g[i].size() == 1){
				x = i;
			} 
		}
		nid[x] = 1;
		for(int i = 2; i <= n; ++ i){
			for(auto j : g[x]){
				if(j.first != ls){
					nid[j.first] = i;
					w[i-1] = j.second;
					ls = x;
					x = j.first;
					break;
				}
			}
		}
		for(int i = 1; i <= k; ++ i){
			ch[i].first = nid[ch[i].first];
		}
		sort(ch + 1, ch + k + 1);
		for(int i = 1; i <= k; ++ i){
			for(int j = i; j <= k; ++ j){
				int flg = 1;
				for(int k = i; k <= j; ++ k){
					if((ch[i].first + ch[k].first) & 1){
						flg = 0;
						break;
					}
				}
				ok[i][j] = flg;
			}
		}
		for(int i = 1; i <= k; ++ i){
			++ sum[i][ch[i].second];
			sum[i][0] += sum[i-1][0];
			sum[i][1] += sum[i-1][1];
		}
		static int f[N][N][N];
		for(int i = 1; i <= k; ++ i){
			for(int j = 1; j <= i; ++ j){
				if(!ok[j][i]){
					continue;
				}
				for(int p = 1; p <= n; ++ p){
					if(i == 1){
						f[j][i][p] = 0;
					}
					int r = j - 1;
					for(int l = 1; l <= r; ++ l){
						if(!ok[l][r]){
							continue;
						}
						for(int q = 1; q < p; ++ q){
							if((p + q + ch[r].first + ch[j].first) & 1){
								continue;
							}
							if(p - q > ch[j].first - ch[r].first){
								continue;
							}
							int val = 0;
							if(q + 1 == p){
								val += (sum[i][1] - sum[j-1][1]) * (sum[r][0] - sum[l-1][0]);
								val += (sum[i][0] - sum[j-1][0]) * (sum[r][1] - sum[l-1][1]);
							}
							f[j][i][p] = max(f[j][i][p], f[l][r][q] + val * w[q]);
						}
					}
				}
			} 
		}
		int ans = 0;
		for(int i = 1; i <= k; ++ i){
			for(int j = 1; j <= k; ++ j){
				for(int p = 1; p <= n; ++ p){
					ans = max(ans, f[i][j][p]);
				}
			}
		}
		printf("%d\n", ans);
	}
	return 0;
} 

19. AT_abc341_g - Highest Ratio [*2208]

\(\dfrac 1{r-k+1}\sum\limits_{i=k}^r A_i=\dfrac{S_r-S_{k-1}}{r-(k-1)}\)

所以从右往左维护 \((i,S_i)\) 构成的上凸壳即可。

点击查看代码
const int N = 2e5 + 10;
int n, st[N], tp, r[N];
typedef long long ll;
ll a[N], sum[N];

bool calc(int x, int y, int z){
	return (sum[x] - sum[z]) * (y - z) >= (sum[y] - sum[z]) * (x - z);
}

int main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; ++ i){
		scanf("%lld", &a[i]);
		sum[i] = sum[i-1] + a[i];
	}
	st[++tp] = n;
	for(int i = n-1; i >= 0; -- i){
		while(tp >= 2 && calc(st[tp-1], st[tp], i)){
			-- tp;
		}
		r[i+1] = st[tp];
		st[++tp] = i;
	}
	for(int i = 1; i <= n; ++ i){
		printf("%.8lf\n", (sum[r[i]] - sum[i-1]) * 1.0 / (r[i] - i + 1));
	}
	return 0;
}

20. 湖北省选模拟 2023 - 环山危路 / road [省选/NOI-]

竞赛图最大流。边数真的很多。

考虑转化为最小割,设割完后起点所在点集为 \(S\),终点所在点集为 \(T\)\(f(S,T)\)\(S,T\) 的割集使 \(T\) 中点不能到 \(S\)。显然有 \(f(S,T)-f(T,S)=\sum_{i\in S}deg_{i,out} - deg_{i,in}\)

又因为竞赛图性质有 \(f(S,T)+f(T,S)=|S|\times |T|\)。所以 \(f(S,T)=\dfrac{|S|\times |T|+\sum_{i\in S}deg_{i,out} - deg_{i,in}}2\)。所以枚举 \(|S|\) 然后贪心地选点即可。

点击查看代码
const int N = 3010;
int n, m, ind[N], oud[N], p[N];
char e[N][N];

bool cmp(int x, int y){
	return oud[x] - ind[x] < oud[y] - ind[y];
}

int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; ++ i){
		scanf("%s", e[i] + 1);
		for(int j = 1; j <= n; ++ j){
			if(e[i][j] == '1'){
				++ ind[j];
				++ oud[i];
			}
		}
		p[i] = i;
	}
	sort(p + 1, p + n + 1, cmp);
	while(m--){
		int ed, k, S = 0, T = n, sum = 0, ans = 0;
		static int s[N], is[N];
		memset(is, 0, sizeof(is));
		scanf("%d%d", &ed, &k);
		for(int i = 1; i <= k; ++ i){
			scanf("%d", &s[i]);
			is[s[i]] = 1;
			++ S;
			-- T;
			sum += oud[s[i]] - ind[s[i]];
		}
		ans = S * T + sum;
		for(int i = 1; i <= n; ++ i){
			if(is[p[i]] || p[i] == ed){
				continue;
			}
			++ S;
			-- T;
			sum += oud[p[i]] - ind[p[i]];
			ans = min(ans, S * T + sum);
		}
		printf("%d\n", ans / 2);
	}
	return 0;
}

21. CF568B - Symmetric and Transitive [*1900]

为什么会有不满足自反性但满足传递性与对称性的二元关系呢?重点在于我们可能没办法对于每个 \(a\) 找到 \(b\) 使得 \(\rho(a,b)\),即把二元关系写成边,构成若干个团和至少一个孤立点(一个点的自环不算孤立点)。

容易发现没有孤立点的话方案数是贝尔数 \(B_n\)link。有孤立点我们枚举非孤立点数量得 \(ans=\sum_{i=0}^{n-1}\dbinom{n}{i} B_i=B_{n+1}-B_n\)

点击查看代码
const int N = 4010;
const ll P = 1e9 + 7;
int n;
ll bell[N][N];

void solve(){
	read(n);
	bell[0][0] = 1;
	for(int i = 1; i <= n; ++ i){
		bell[i][0] = bell[i-1][i-1];
		for(int j = 1; j <= i; ++ j){
			bell[i][j] = (bell[i-1][j-1] + bell[i][j-1]) % P;
		}
	}
	println(bell[n][n-1]);
}

22. ARC162F - Montage [*3190]

*3190? *2190!

首先可以钦定 \(i\)\(j\) 列有 \(1\),其他列没有 \(1\),最后算出方案后乘上组合数更新答案。根据题目,若一个矩形左上、右下两个位置是 \(1\),则四个角都要是 \(1\),所以我们可以发现合法的方案是左下到右上的两个递增阶梯 \(a,b\)\(b\) 中所有位置都在 \(a\),中,再把在 \(a\) 中不在 \(b\) 中的位置设为 \(1\)

image

那么就可以 dp:设 \(f_{i,l,r}\) 表示第 \(i\) 列,\([l,r]\) 行为 \(1\) 的方案数,发现可以转移到 \(f_{i,l,r}\)\(f_{i-1,p,q}\) 需满足 \(p\leq l,\max(1,l-1)\leq q\leq r\),是一个矩形,可以二维前缀和优化。

点击查看代码
const int N = 410;
const ll P = 998244353;
int n, m;
ll C[N][N], f[N][N], g[N][N];
ll sum[N][N];

int main(){
    scanf("%d%d", &n, &m);
    C[0][0] = 1;
    if(n < m){
        swap(n, m);
    }
    for(int i = 1; i <= n; ++ i){
        C[i][0] = C[i][i] = 1;
        for(int j = 1; j < i; ++ j){
            C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
        }
    }
    for(int i = 1; i <= m; ++ i){
        f[1][i] = g[1][i] = 1;
    }
    for(int l = 1; l <= m; ++ l){
        for(int r = 1; r <= m; ++ r){
            sum[l][r] = sum[l-1][r] + sum[l][r-1] - sum[l-1][r-1] + f[l][r] + P;
            while(sum[l][r] >= P) sum[l][r] -= P;
        }
    }
    for(int i = 2; i <= n; ++ i){
        for(int l = 1; l <= m; ++ l){
            for(int r = l; r <= m; ++ r){
                f[l][r] = sum[l][r] - sum[l][max(0, l-2)] + P;
                while(f[l][r] >= P) f[l][r] -= P;
            }
        }
        for(int l = 1; l <= m; ++ l){
            for(int r = 1; r <= m; ++ r){
                sum[l][r] = sum[l-1][r] + sum[l][r-1] - sum[l-1][r-1] + f[l][r] + P;
                while(sum[l][r] >= P) sum[l][r] -= P;
            }
        }
        for(int j = 1; j <= m; ++ j){
            for(int p = 1; p <= j; ++ p){
                g[i][j] = g[i][j] + f[p][j];
                while(g[i][j] >= P) g[i][j] -= P;
            }
        }
    }
    ll ans = 1;
    for(int i = 1; i <= n; ++ i){
        for(int j = 1; j <= m; ++ j){
            ans = (ans + C[n][i] * C[m][j] % P * g[i][j]) % P;
        }
    }
    printf("%lld\n", ans);
    return 0;
}

23. AGC046F - Forbidden Tournament [*3854]

引理 1. 竞赛图缩点后形成一条链。

引理 2. 竞赛图的任意一个点数 \(\geq 3\) 的 SCC 中存在三元环 \((u,v)(v,w)(w,u)\)

两个引理都是竞赛图的基本性质。

对这个竞赛图进行缩点,设链上的 SCC 点集依次为 \(s_1,s_2,...,s_p\),则若存在 \(i\) 使 \(i<p,|s_i|\ge 3\),则选择一个属于该 SCC 的三元环 \((u,v,w)\) 以及 \(s_p\) 中的一个点 \(z\)\((u,v,w,z)\) 构成一个子图。所以 \(\forall i<p,|s_i|<3\);又因为不存在大小为 \(2\) 的 SCC,所以 \(\forall i<p,|s_i|=1\)

枚举 \(p\),问题转化为求大小为 \(N-p-1\)、不存在给定子图、每个点入度 \(\leq K-p-1\) 的 SCC 的数量。设 \(n=N-p-1,m=K-p-1\)

考虑任选 SCC 中一点 \(x\),则其余 \(n-1\) 个点可以被分为两个集合 \(S,T\),其中 \(S\) 中点都有连向 \(x\) 的边,\(x\) 所有连出去的边都连向 \(T\) 中的点。由于 \(x\) 在 SCC 中,所以 \(|S|,|T|\geq 1\)

容易发现 \(S\) 一定构成一条链。否则 \(S\) 中会存在大小 \(\geq 3\) 的 SCC,其中取出一个三元环,这三个点与 \(x\) 会构成一个子图。

\(T\) 也一定构成一条链。否则 \(T\) 中会存在大小 \(\geq 3\) 的 SCC,其中取出三元环 \((u,v,w)\)。设 \(S\) 链的第一个节点为 \(s\)\(T\) 的子集 \(T_1\) 中的点连向了 \(s\)\(s\) 连向了 \(T\) 的子集 \(T_2\) 中的点。若 \(|T_2|=0\),则 \(u,v,w\in T_1\)\((u,v,w,s)\) 构成子图;若 \(|T_1|=0\),则整个图构成不了 SCC;若存在 \(a\in T_1,b\in T_2\) 使得存在 \((a,b)\),则 \((a,s,x,b)\) 构成子图;否则不存在 \(T_2\)\(T_1\) 连的边,图构成不了 SCC。

把两条链上的节点按照拓扑序重标号。可以发现对于 \(s_i\in S\),其连向 \(T\) 的节点一定是一个前缀。否则存在 \(t_j,t_{j+1}\in T\) 存在边 \((t_j,s_i),(s_i,t_{j+1})\)\((s_i,t_j,x,t_{j+1})\) 构成子图。设 \(p_i\) 表示 \(\forall j\leq p_i\) 存在边 \((s_i,t_j)\) 且不存在 \((s_i,t_{j+1})\)

可以发现 \(p_i\leq p_{i+1},p_1< |T|\)

\(p_1=|T|\),则图构成不了 SCC。

\(p_i>p_{i+1}\)

  • \(p_i<|T|\),则 \((t_{p_i},t_{p_{i+1}},s_i,s_{i+1})\) 构成子图。
  • \(p_i=|T|\),则 \((t_{|T|},s_1,s_i,s_{i+1})\) 构成子图。

所以就可以 dp。设 \(f_{i,j}\) 表示左边第 \(i\) 个点的 \(p_i=j\) 的方案数。枚举左边点数 \(p\),则右边点数为 \(n-p-1\)。转移时用 \(f_{i-1,k}\) 更新 \(f_{i,j}(k\leq j)\)

注意一个状态 \(f_{i,j}\) 可行当且仅当:

  • \(i\neq 1\)\(j\neq q\)
  • \((q-j)+(i-1) \leq m\),即 \(s_i\) 的入度 \(\leq m\)
  • \((p-i+1)+1+(j-1) \leq m\),即 \(t_j\) 的入度 \(\leq m\)

最后设 \(sum=\sum_{i=0}^q f_{p,i}\),即为左边 \(p\) 个点右边 \(q\) 个点的方案数。对答案的贡献要乘上 \(\dbinom{n-1}pp!q!\),因为两边点可以随便排列,还要选择每个点到底在哪一边。注意这里是选择了中间点 \(x\),所以 \(x\) 是唯一的,不用管。

现在求出 \(ans\)\(n=N-p-1,m=K-p-1\) 的答案,对总答案的贡献要乘上 \(\dbinom npp!\),表示选择前面链上的若干个点。

点击查看代码
const int N = 210;
ll P, C[N][N], fac[N], ans, f[N][N];
int n, m;

ll calc(int n, int m){
    ll res = 0;
    for(int p = 1; p < n - 1; ++ p){
        int q = n - 1 - p;
        ll sum = 0;
        f[1][q] = 0;
        if(p > m || q > m){
            continue;
        }
        for(int i = 0; i < q; ++ i){
            f[1][i] = 1;
            if(q-i > m || (i && p+i > m)){
                f[1][i] = 0;
            }
        }
        for(int i = 2; i <= p; ++ i){
            ll pr = 0;
            for(int j = 0; j <= q; ++ j){
                pr = (pr + f[i-1][j]) % P;
                f[i][j] = pr;
                if(i-1+q-j > m || (j && p-i+1+j > m)){
                    f[i][j] = 0;
                }
            }
        }
        for(int i = 0; i <= q; ++ i){
            sum = (sum + f[p][i]) % P;
        }
        res = (res + C[n-1][p] * fac[p] % P * fac[q] % P * sum) % P;
    }
    return res;
}

int main(){
    scanf("%d%d%lld", &n, &m, &P);
    fac[0] = C[0][0] = 1;
    for(int i = 1; i <= n; ++ i){
        fac[i] = fac[i-1] * i % P;
        C[i][0] = C[i][i] = 1;
        for(int j = 1; j < i; ++ j){
            C[i][j] = (C[i-1][j-1] + C[i-1][j]) % P;
        }
    }
    if(m == n - 1){
        ans = fac[n];
    }
    for(int i = 0; i <= min(m, n - 3); ++ i){
        ans = (ans + fac[i] * C[n][i] % P * calc(n-i, m-i)) % P;
    }
    printf("%lld\n", ans);
    return 0;
}

24. BJOI2014 - 大融合 [省选/NOI-]

首先离线,把树建出来。若不连通在最后补边使得连通。

然后遍历操作:

  • 若为添加操作 \((x,y)\),不妨 \(x\)\(y\) 的父亲,则使用并查集维护 \(x\) 到最浅的一个节点 \(tp\) 使得 \(x\to tp\) 的路径都是添加过的。然后用树剖将这些点的 \(siz\) 都加上 \(siz_y\)

  • 若为查询操作 \((x,y)\),不妨 \(x\)\(y\) 的父亲,则两部分大小分别为 \(siz_{tp_x}-siz_y,siz_y\)

点击查看代码
const int N = 2e5 + 10;
int n, q, fa[N], dep[N], siz[N], anc[N], sz[N], son[N], dfn[N], top[N];
tuple<int, int, int> op[N];
vector<int> g[N];

int gf(int x){
    return x == fa[x] ? x : fa[x] = gf(fa[x]);
}
void dfs(int x, int fat){
    sz[x] = 1;
    dep[x] = dep[fat] + 1;
    anc[x] = fat;
    for(int i : g[x]){
        if(i == fat){
            continue;
        }
        dfs(i, x);
        sz[x] += sz[i];
        if(sz[i] > sz[son[x]]){
            son[x] = i;
        }
    }
}
void dfss(int x, int tp){
    dfn[x] = ++ *dfn;
    top[x] = tp;
    if(son[x]){
        dfss(son[x], tp);
    }
    for(int i : g[x]){
        if(i != anc[x] && i != son[x]){
            dfss(i, i);
        }
    }
}

ll t[N*4], tag[N*4];
void psd(int p, int l, int r){
    int mid = l + r >> 1;
    t[p<<1] += tag[p] * (mid - l + 1);
    t[p<<1|1] += tag[p] * (r - mid);
    tag[p<<1] += tag[p];
    tag[p<<1|1] += tag[p];
    tag[p] = 0;
}
void add(int p, int l, int r, int ql, int qr, ll v){
    if(qr < l || r < ql){
        return;
    } else if(ql <= l && r <= qr){
        t[p] += v * (r - l + 1);
        tag[p] += v;
    } else {
        int mid = l + r >> 1;
        psd(p, l, r);
        add(p<<1, l, mid, ql, qr, v);
        add(p<<1|1, mid+1, r, ql, qr, v);
        t[p] = t[p<<1] + t[p<<1|1];
    }
}
ll qry(int p, int l, int r, int x){
    if(l == r){
        return t[p];
    } else {
        int mid = l + r >> 1;
        psd(p, l, r);
        if(x <= mid){
            return qry(p<<1, l, mid, x);
        } else {
            return qry(p<<1|1, mid+1, r, x);
        }
    }
}

void add(int x, int y, ll v){
    while(top[x] != top[y]){
        if(dep[top[x]] < dep[top[y]]){
            swap(x, y);
        }
        add(1, 1, n, dfn[top[x]], dfn[x], v);
        x = anc[top[x]];
    }
    if(dep[x] > dep[y]){
        swap(x, y);
    }
    add(1, 1, n, dfn[x], dfn[y], v);
}

int main(){
    scanf("%d%d", &n, &q);
    for(int i = 1; i <= n; ++ i){
        fa[i] = i;
    }
    for(int i = 1; i <= q; ++ i){
        char ch[5];
        int x, y;
        scanf("%s%d%d", ch, &x, &y);
        op[i] = { (ch[0] == 'A' ? 0 : 1), x, y };
        if(ch[0] == 'A'){
            g[x].push_back(y);
            g[y].push_back(x);
            fa[gf(x)] = gf(y);
        }
    }
    for(int i = 1; i <= n; ++ i){
        if(i == gf(i) && gf(i) != gf(1)){
            op[++q] = { 0, 1, i };
            g[1].push_back(i);
            g[i].push_back(1);
        }
    }
    dfs(1, 0);
    dfss(1, 1);
    for(int i = 1; i <= n; ++ i){
        fa[i] = i;
    }
    add(1, 1, n, 1, n, 1);
    for(int i = 1; i <= q; ++ i){
        int x = get<1>(op[i]), y = get<2>(op[i]);
        if(dep[x] > dep[y]){
            swap(x, y);
        }
        if(get<0>(op[i]) == 0){
            int tp = gf(x);
            int sy = qry(1, 1, n, dfn[y]);
            add(tp, x, sy);
            fa[y] = tp;
        } else {
            int tp = gf(x);
            int all = qry(1, 1, n, dfn[tp]);
            int sy = qry(1, 1, n, dfn[y]);
            printf("%lld\n", (ll)(all - sy) * sy);
        }
    }
    return 0;
}

25. ARC162E - Strange Constraints [*2780]

考虑 dp 顺序:按数出现在 \(b\) 数组中的次数从大到小 dp。

\(f_{i,j,k}\) 表示 \(cnt\geq i\),填了 \(j\) 种数,\(k\) 个位置的方案数。\(pr_i\) 表示 \(\sum[a_j\geq i]\),则出现次数 \(\geq i\) 的数只能填在 \(pr_i\) 个位置上且只有 \(pr_i\) 个数出现次数可以 \(\geq i\)

发现随着 \(i\) 的减小,可填的区间逐步增加且完全包含更大的 \(i\) 可填的区间。所以 dp 方程:

\(f_{i,j+x,k+ix}=\sum_{x}f_{i+1,j,k}\times\dbinom{pr_i-j}{x}\dfrac{(pr_i-k)!}{(pr_i-k-ix)![(i)!]^k}\)

其中左边组合数表示从 \(pr_i\) 个可行数去掉已经选了的 \(j\) 个数中选择 \(x\) 个数。右边分式表示从 \(pr_i-k\) 个位置填上 \(ix\) 个数,且这些数分为 \(i\) 个集合,每个集合内无序的可重集方案数。

分析复杂度:看似是 \(O(n^4)\),其实 \(j,x\leq \dfrac ni\)

所以总共的循环次数大约为 \(\sum_{i=1}^n\dfrac{n^3}{i^2}=O(n^3)\)

点击查看代码
const int N = 510;
const ll P = 998244353;
int n, a[N], pr[N];
ll C[N][N], fac[N], inv[N], f[N][N][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;
}

void solve(){
	read(n);
	for(int i = 1; i <= n; ++ i){
		read(a[i]);
	}
	fac[0] = C[0][0] = inv[0] = 1;
	for(int i = 1; i <= n; ++ i){
		fac[i] = fac[i-1] * i % P;
		inv[i] = qp(fac[i], P-2);
		C[i][0] = C[i][i] = 1;
		for(int j = 1; j < i; ++ j){
			C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
		}
		for(int j = 1; j <= n; ++ j){
			if(a[j] >= i){
				++ pr[i];
			}
		}
	}
	f[n+1][0][0] = 1;
	for(int i = n; i >= 1; -- i){
		for(int j = 0; i * j <= n && j <= pr[i]; ++ j){
			for(int k = 0; k <= pr[i]; ++ k){
				if(!f[i+1][j][k]){
					continue;
				}
				ll pw = 1;
				for(int x = 0; k + x * i <= pr[i] && j + x <= n; ++ x){
					ll fz = C[pr[i]-j][x] * fac[pr[i]-k] % P;
					ll fm = pw * inv[pr[i]-k-x*i] % P;
					(f[i][j+x][k+i*x] += f[i+1][j][k] * fz % P * fm) %= P;
					pw = pw * inv[i] % P;
				}
			}
		}
	}
	ll ans = 0;
	for(int i = 0; i <= n; ++ i){
		(ans += f[1][i][n]) %= P;
	}
	println(ans);
}

26. ARC112F - Die Siedler [*3432]

观察到,若可以进行操作 \(1\),那么一次操作 \(1\) 一定是更优的。所以我们先把原状态退回到全是第 \(1\) 种牌的情况,则这个状态只进行操作 1 到达的最优情况只和第 \(1\) 种牌的数量有关。

一个状态对应的第 \(1\) 种牌的数量为:\(\sum_{i=1}^nc_i\times2^i\times i!\)。设初始状态的数量为 \(st\),对应每个牌堆的状态为 \(vl_i\)。则我们每次可以让 \(st\) 加上任意一个 \(vl_i\);又因为每 \(2^n\times n!\) 个第 \(1\) 种牌又可以合成 \(1\) 个第 \(1\) 种牌,所以每次还可以让 \(st\) 减去 \(2^n\times n!-1\)

所以最终可以到达的状态 \(ed\) 可以表示为 \(st+\sum_{i=1}^nx_ic_i-y(2^nn!-1)\)。其中 \(x_i,y\) 为任意自然数。由裴蜀定理,\(ed\) 可以到达的充要条件为 \(ed\equiv st\pmod g\),其中 \(g=\gcd(\gcd_{i=1}^nvl_i,(2^nn!-1))\)

\(cnt(x)\) 表示 \(x\) 张第 \(1\) 种牌经过若干次操作 \(1\) 的最小牌数。

这时候我们有两种方法:

  • 暴力枚举 \(ed=st\bmod g+kg(ed>0,k\in\mathbb N)\),计算 \(cnt(ed)\) 最小值。复杂度 \(O(\dfrac{2^nn!-1}g)\)
  • 同余最短路,建 \(g\) 个点,每个点 \(x\) 表示 \(a\equiv x\pmod g\) 的最小的 \(cnt(a)\)。初始状态 \(dis_{2^ii!\bmod g}=1\),因为 \(dis_0\) 不能取到 \(0\)。连边 \(i\to (i+2^jj!)\bmod p\),边权为 \(1\),表示添加一张第 \(j\) 种牌。最后答案取 \(dis_{st\bmod g}\)。复杂度 \(O(g)\)

总复杂度 \(O(\sqrt{2^nn!-1})\)。但是小于 \(\sqrt{2^nn!-1}\) 且为 \({2^nn!-1}\) 因数的最大值为 \(1214827\),当 \(n=12\) 时取到。所以 \(g\leq1214827\) 或者 \(g\geq\dfrac{2^nn!-1}{1214827}\)。可以过。

点击查看代码
const int P = 1214827;
int n, m, c[20], s[55][20], d[P+10];
ll vl[20], fac[20], pw[20];

ll clc(int x[]){
    ll rs = 0;
    for(int i = 1; i <= n; ++ i){
        rs += x[i] * (1ll << i-1) * fac[i-1];
    }
    return rs;
}
int cnt(ll x){
    int ans = 0;
    for(int i = 1; i <= n; ++ i){
        ans += x % (i + i);
        x /= i + i;
    }
    return ans;
}

int main(){
    scanf("%d%d", &n, &m);
    fac[0] = 1;
    for(int i = 1; i <= n; ++ i){
        scanf("%d", &c[i]);
        fac[i] = fac[i-1] * i;
    }
    ll st = clc(c), mx = (1ll << n) * fac[n] - 1;
    ll g = mx;
    for(int i = 1; i <= m; ++ i){
        for(int j = 1; j <= n; ++ j){
            scanf("%d", &s[i][j]);
        }
        vl[i] = clc(s[i]);
        g = __gcd(g, vl[i]);
    }
    if(g >= P){
        int ans = 1e9;
        ll m = st % g == 0 ? g : st % g;
        for(ll i = m; i <= mx; i += g){
            ans = min(ans, cnt(i));
        }
        printf("%d\n", ans);
    } else {
        memset(d, -1, sizeof(d));
        queue<int> q;
        for(int i = 0; i <= n; ++ i){
            d[fac[i]*(1ll<<i)%g] = 1;
            q.push(fac[i]*(1ll<<i)%g);
        }
        while(q.size()){
            int x = q.front();
            q.pop();
            for(int i = 0; i <= n; ++ i){
                int y = (x + fac[i] * (1ll << i)) % g;
                if(d[y] == -1){
                    d[y] = d[x] + 1;
                    q.push(y);
                }
            }
        }
        printf("%d\n", d[st%g]);
    }
    return 0;
}

27. ARC112E - Cigar Box [*2659]

考虑移动过的数集合 \(S\)。那么目标数列中 \(S\) 中的数的位置一定为一个前缀和一个后缀,其它的部分是一个上升的序列。

所以枚举 \(L,R\) 表示前缀、后缀的长度,如果 \([L+1,n-R]\) 是上升的则统计答案;否则答案为 \(0\)

我们将 \(m\) 次操作分配给这 \(L+R\) 个数,方案数是 \({m \brace {L+R}}\binom{L+R}{L}\)。然后这些操作中只有每个数的最后一次有方案限制,其它两个方向都可以,乘上 \(2^{m-L-R}\)

点击查看代码
const int N = 3010;
typedef long long ll;
const ll P = 998244353;
int n, m, a[N], pr[N];
ll C[N][N], S[N][N], pw[N];

int main(){
    scanf("%d%d", &n, &m);
    a[0] = n+1;
    C[0][0] = S[0][0] = pw[0] = 1;
    for(int i = 1; i <= n; ++ i){
        scanf("%d", &a[i]);
        if(a[i] > a[i-1]){
            pr[i] = pr[i-1];
        } else {
            pr[i] = i;
        }
    }
    for(int i = 1; i <= m; ++ i){
        C[i][0] = 1;
        S[i][0] = 0;
        pw[i] = pw[i-1] * 2 % P;
        for(int j = 1; j <= i; ++ j){
            S[i][j] = (S[i-1][j-1] + S[i-1][j] * j) % P;
            C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
        }
    }
    ll ans = 0;
    for(int L = 0; L <= n; ++ L){
        for(int R = 0; L + R <= min(n, m); ++ R){
            if(pr[n-R] <= L + 1){
                ans = (ans + S[m][L+R] * C[L+R][R] % P * pw[m-L-R]) % P;
            }
        }
    }
    printf("%lld\n", ans);
    return 0;
}

28. CF1924D - Balanced Subsequences [*2700]

题意:求所有 \(n\) 个左括号,\(m\) 个右括号,最多能匹配 \(k\) 组的括号串个数。

有一个结论是只要 \(n,m\geq k\),则这个方案数只与 \(n+m,k\) 有关。

考虑把 ( 写成向量 \((1,1)\)) 写成向量 \((1,-1)\),则括号串可与 \((0,0)\to(n+m,n-m)\) 的一条折线构成双射;括号串最多 \(k\) 组匹配等价于折线最低点纵坐标为 \(k-m\)(从 \(x\) 轴所有位置向右边发一束光,光能照到的 \((1,-1)\) 向量共 \(m-k\) 个一定不能匹配,其它的都可以匹配,则得证)。

最低点纵坐标为 \(k-m\) 不好做,考虑求最低点纵坐标 \(\leq k-m\),即匹配数 \(\leq k\) 的方案数。

考虑折线与 \(x=k-m\) 的直线的最后一个交点,并把之后的部分翻转。因为 \(x=n-m\)\(x=k-m\) 的距离为 \(n-k\),所以翻转过后折线终点会变为 \((n+m,2k-n-m)\)。折线总数为 \(\dbinom{n+m}{k}\)。这种折线可以与最低点纵坐标 \(\leq k-m\) 的折线构成双射。

所以答案为 \(\dbinom{n+m}k-\dbinom{n+m}{k-1}\)

another problem

\(n\),对于所有 \(k\in[0,n]\),求 \(k\) 个左括号 \(n−k\) 个右括号构成的括号序列,最长合法括号子序列的长度之和。

考虑递推,设 \(f_i\) 表示 \(k=i\) 时的答案。则 \(f_i\)\(i\) 个左括号构成最长合法括号子序列长度 \(<i,=i\) 的答案加起来。由于上文提到的结论,那么 \(<i\) 的答案就是 \(f_{i-1}\)!然后 \(=i\) 的部分就变成了上面的题,方案数是 \(\dbinom{n}{i}-\dbinom{n}{i-1}\),贡献是 \(i(\dbinom{n}{i}-\dbinom{n}{i-1})\)

这个递推只适用于 \(2i\leq n+1\) 的情况。剩余的部分对称即可。

点击查看代码
const ll P = 1e9 + 7;
const int N = 5e3 + 10;
int n;
ll c, fac[N], inv[N], f[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;
}
ll C (int x, int y) {
    return fac[x] * inv[y] % P * inv[x-y] % P;
}

int main () {
    n = 5000;
    fac[0] = 1;
    for (int i = 1; i <= n; ++ i) {
        fac[i] = fac[i-1] * i % P;
    }
    inv[n] = qp (fac[n], P-2);
    for (int i = n - 1; i >= 0; -- i) {
        inv[i] = inv[i+1] * (i + 1) % P;
    }
    int t, n, m, k;
    scanf("%d", &t);
    while(t--){
        scanf("%d%d%d", &n, &m, &k);
        if(k > min(n, m)){
            puts("0");
            continue;
        }
        printf("%lld\n", (C(n+m, k) - C(n+m, k-1) + P) % P);
    }
    return 0;
}

29. AT_dwacon5th_prelims_e - Cyclic GCDs [*2978]

将数组从小到大排序。设 \(f_{i,j}\) 表示前 \(i\) 个数划分为 \(j\) 个圆排列的答案。容易得到递推式:

\(f_{i,j}=f_{i-1,j-1}*a_i+(i-1)*f_{i-1,j}\)

考虑生成函数 \(G_i=\sum_{j=1}^nf_{i,j}x^j\),则有:

\(G_i=a_ixG_{i-1}+(i-1)G_{i-1}=G_{i-1}(a_ix+i-1)=\prod_{j=1}^i(a_jx+j-1)\)

\(b_i=[x^i]G_n\),则答案为 \(\gcd_{i=1}^n[x^i]G_n\)

有结论:设 \(c(F)\) 表示 \(\gcd_i[x^i]F\),则 \(c(FG)=c(F)c(G)\)

proof

考虑 \(c(F)=c(G)=1\) 时,若 \(c(FG)=p\neq1\),则 \(FG\equiv0\pmod p\),又 \(F,G\not\equiv0\pmod p\),矛盾。则 \(c(FG)=1\)

所以 \(\gcd_{i=1}^n[x^i]G_n=\prod_{i=1}^n\gcd(a_i,i-1)\)

点击查看代码
int n, a[100010];
const ll P = 998244353;

void solve(){
	int ans = 1;
	read (n);
	for (int i = 1; i <= n; ++ i) {
		read (a[i]);
	}
	sort (a + 1, a + n + 1);
	for (int i = 1; i <= n; ++ i) {
		ans = (ll)ans * __gcd (i - 1, a[i]) % P;
	}
	println (ans);
}

30. JSOI2011 - 柠檬 [省选/NOI-]

可以发现对于最优方案,每个选择的区间两边端点颜色相同。所以 设 \(f_i\) 表示 \([1,i]\) 分成若干区间的最优答案。则 \(f_i\) 只会从 \(a_{j+1}=a_i\)\(f_j\) 转移过来。设 \(cnt_i\) 表示位置 \(i\) 是数组中出现的第几个 \(a_i\)

\(f_i=\max_{a_{j}=a_i}\{f_{j-1}+a_i(cnt_i-cnt_j+1)^2\}\)

拆开:

\(f_i=a_icnt_i^2+\max_{a_j=a_i}\{f_{j-1}+a_jcnt_j^2+2cnt_j\times(-cnt_i-1)\}\)

可以把每个颜色分开来斜率优化。每个点的坐标为 \((2cnt_j,f_{j-1}+a_jcnt_j^2)\),考虑是上凸还是下凸/取首位还是末位:

  • 由于斜率 \(-cnt_i-1\) 变小,所以维护下凸;
  • 由于要取下凸最大值,所以答案取栈顶。

所以维护若干单调栈:

  • 弹出栈顶若干元素并插入 \(i\)
  • 弹出栈顶若干元素,使得栈顶转移最优。
点击查看代码
const int N = 1e5 + 10;
int n, ls[N];
ll f[N], a[N], cnt[N];
vector<int> g[N];

ll X(int x){
	return 2 * a[x] * cnt[x];
}
ll Y(int x){
	return f[x-1] + a[x] * cnt[x] * cnt[x];
}

int main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; ++ i){
		scanf("%d", &a[i]);
		vector<int> &nw = g[a[i]];
		int p = nw.size();
		cnt[i] = cnt[ls[a[i]]] + 1;
		ls[a[i]] = i;
		int k = nw.size();
		while(p >= 2 && 
		      (Y(i)-Y(nw[p-1])) * (X(nw[p-1]) - X(nw[p-2])) >= 
			  (Y(nw[p-1])-Y(nw[p-2])) * (X(i) - X(nw[p-1]))){
			nw.pop_back();
			-- p;
		}
		nw.push_back(i);
		++ p;
		while(p >= 2 &&
		      Y(nw[p-1]) - (cnt[i] + 1) * X(nw[p-1]) <= Y(nw[p-2]) - (cnt[i] + 1) * X(nw[p-2])){
		    nw.pop_back();
		    -- p;
		}
		int j = nw[p-1];
		f[i] = Y(j) - (cnt[i] + 1) * X(j) + a[i] * (cnt[i] + 1) * (cnt[i] + 1); 
	}
	printf("%lld\n", f[n]);
	return 0;
} 

31. 省选联考 2022 - 卡牌 [省选/NOI-]

由于值域只有 \(2000\),每个数至多包含一个 \(\geq 47\) 的质因数,考虑把质数分成 \(\leq 43\)\(\geq 47\) 两部分处理。对于每个数预处理出质因数拆分 \((st,x)\),其中 \(st\) 为一个 \(14\) 位的二进制数,表示是否包含 \([2,43]\) 内的质数,\(x\) 表示这个数除掉 \(\leq 43\) 的质数后剩余的部分。然后把这些数按照 \(x=1,47,53,...\) 分成若干类,每一类分别计算后合并答案。

简单的 dp 是设 \(f_{i,j}\) 表示第 \(i\) 类中的数乘积包含状态 \(j\) 的质因数的方案数。这个很容易 dp,但是并不容易合并。由之前模拟赛爆炸的经验得这里可以先容斥,合并后再统计回来。设 \(f_{i,j}\) 表示第 \(i\) 类中的数乘积不包含状态 \(j\) 的质因数的方案数。我们可以先 dp 出 \(g_{i,j}\) 表示第 \(i\) 类中的数乘积不包含状态 \(j\) 的质因数,且包含除掉状态 \(j\) 以外的所有 \([2,43]\) 内的质因数的方案数,然后利用 FWT 即可算出 \(f_{i,j}\)

由于 \(f_{i,j}\) 的定义,我们要合并两个状态 \(p,q\) 直接 \(f_{p,j}\times f_{q,j}\) 即可。但是还有一个细节:\(f_{i,j}\) 并没有要求要包含第 \(i\) 类表示的质因数 。所以对于一个询问的质因数集合 \(S\),若第 \(i\) 类表示的质因数在 \(S\) 中,每个 \(f_i\) 需要减一,表示去掉选择空集的方案。

容斥系数为 \((-1)^{\operatorname{popcount}(j)}\)

点击查看代码
const int N = 2010;
const ll P = 998244353;
int n, val[N], m, p[N], pr, pid[N], qid[N], v[300];
vector<pair<int, int> > G[N];
int f[300][1<<14], g[1<<14], pw2[1000010];

int main(){
	n = 1e6;
	pw2[0] = 1;
	for(int i = 1; i <= n; ++ i){
		pw2[i] = pw2[i-1] * 2 % P;
	}
	scanf("%d", &n);
	for(int i = 1, a; i <= n; ++ i){
		scanf("%d", &a);
		++ val[a];
	}
	pid[1] = p[1] = p[0] = 1;
	for(int i = 2; i <= 2000; ++ i){
		bool flg = 1;
		for(int j = 2; j * j <= i; ++ j){
			if(i % j == 0){
				flg = 0;
				break;
			}
		}
		if(flg){
			qid[i] = *p - 1;
			p[++*p] = i;
			if(i == 43){
				pr = *p;
			} else if(i > 43){
				pid[i] = *p - pr + 1;
			}
		}
	}
	for(int i = 1; i <= 2000; ++ i){
		int x = i, st = 0;
		for(int j = 2; j <= pr; ++ j){
			while(x % p[j] == 0){
				x /= p[j];
				st |= (1 << j-2);
			}
		}
		if(val[i]){
			G[pid[x]].emplace_back(st, val[i]);
		}
	}
	for(int i = 1; i <= 290; ++ i){
		f[i][0] = 1;
		for(auto j : G[i]){
			int x = j.first, v = j.second;
			memcpy(g, f[i], sizeof(g));
			for(int k = 0; k < (1 << 14); ++ k){
				f[i][k|x] = (f[i][k|x] + (ll)g[k] * (pw2[v]-1)) % P;
			}
		}
		for(int j = 0; j < (1 << 14); ++ j){
			g[j] = f[i][(1<<14)-1-j];
		}
		for(int j = 0; j < (1 << 14); ++ j){
			f[i][j] = g[j];
		}
		for(int j = 0; j < 14; ++ j){
			for(int k = 0; k < (1 << 14); ++ k){
				if(k & (1 << j)){
					f[i][k^(1<<j)] += f[i][k];
					if(f[i][k^(1<<j)] >= P){
						f[i][k^(1<<j)] -= P;
					}
				}
			}
		}
	}
	scanf("%d", &m);
	while(m--){
		int c, x, st = 0;
		scanf("%d", &c);
		memset(v, 0, sizeof(v));
		for(int i = 1; i <= c; ++ i){
			scanf("%d", &x);
			if(x <= 43){
				st |= (1 << qid[x]);
			} else {
				v[pid[x]] = 1;
			}
		}
		ll ans = 0;
		for(int j = st; j >= 0; j = (j - 1) & st){
			#define gg(x) (((x)&1) ? P-1 : 1)
			ll tmp = gg(__builtin_popcount(j));
			for(int i = 1; i <= 290; ++ i){
				ll op = v[i] ? P - 1 : 0;
				tmp = tmp * (f[i][j] + op) % P;
			}
			ans += tmp;
			if(ans >= P){
				ans -= P;
			}
			if(!j){
				break;
			}
		}
		printf("%lld\n", ans);
	}
	return 0;
}

32. BJWC2018 - 最长上升子序列 [省选/NOI-]

所有 LIS 长度为 \(k\)\(n\) 阶排列个数等价于第一行 \(k\) 个格子的杨表数的平方。所以我们枚举杨图,并使用勾长公式计算出杨图对应的杨表数即可。复杂度 \(n^3\) 乘上 \(n\) 的拆分数,可以过。

勾长公式:\(cnt=\dfrac{n!}{\prod d_{i,j}}\)

点击查看代码
const ll P = 998244353;
int a[30], n;
ll ans, fac = 1;

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 mx, int dep){
	if(x == n){
		ll mul = 1;
		for(int i = 1; i <= dep; ++ i){
			for(int j = 1; j <= a[i]; ++ j){
				int sum = a[i] - j;
				for(int k = i; k <= dep && a[k] >= j; ++ k){
					++ sum;
				}
				mul = mul * sum % P;
			}
		}
		ans = (ans + qp(fac * qp(mul, P-2) % P, 2) * a[1] % P) % P;
	} else {
		for(int j = 1; j <= mx && j + x <= n; ++ j){
			a[dep+1] = j;
			dfs(x+j, j, dep+1);
		}
	}
}


void solve(){
	read(n);
	for(int i = 1; i <= n; ++ i){
		fac = fac * i % P;
	}
	dfs(0, n, 0);
	println(ans * qp(fac, P-2) % P);
}

33. POI201 - Lightning Conductor [省选/NOI-]

\(a_j\leq a_i+p_i-\sqrt{|i-j|}\) 可得 \(p_i\geq a_j+\sqrt{|i-j|}-a_i\)。把 \(>j,<j\)\(i\) 分开来得:

\(p_i=\max\{\max_{j<i}a_j+\sqrt{i-j}-a_i,\max_{j>i}a_j+\sqrt{j-i}-a_i\}\)

首先只处理左半部分,设 \(f_j(x)=a_j+\sqrt{x-j}\),则每次转移要选使得 \(f_j(i)\) 最大的 \(j\) 转移到 \(i\)

把这些函数图像画出来得:

image

发现最优的函数一定会是反超之前的所有函数,后来又被新出现的函数反超。

所以维护一个单调队列,其中相邻两个函数的交点横坐标递增。插入一个函数的时候求出它与队首函数的交点,然后把交点横坐标大于这个交点的队内函数出队。

转移的时候查看队首的若干个交点,把交点横坐标小于 \(i\) 的出队,最后取队首转移即可。

点击查看代码
const int N = 5e5 + 10;
int n, a[N], p[N], q[N];
double sq[N];

double fx(int p, int x){
    return a[p] + sq[x-p]; 
}
int crs(int x, int y){
    int l = y, r = n + 1;
    while(l < r){
        int mid = l + r >> 1;
        if(fx(x, mid) > fx(y, mid)){
            l = mid + 1;
        } else {
            r = mid;
        }
    }
    return l;
}

int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; ++ i){
        sq[i] = sqrt(i);
        scanf("%d", &a[i]);
    }
    int l = 1, r = 0;
    for(int i = 1; i <= n; ++ i){
        while(l < r && crs(q[r-1], q[r]) >= crs(q[r], i)){
            -- r;
        }
        q[++r] = i;
        while(l < r && crs(q[l], q[l+1]) <= i){
            ++ l;
        }
        p[i] = ceil(fx(q[l], i)) - a[i];
    }
    l = 1, r = 0;
    reverse(a + 1, a + n + 1);
    reverse(p + 1, p + n + 1);
    for(int i = 1; i <= n; ++ i){
        while(l < r && crs(q[r-1], q[r]) >= crs(q[r], i)){
            -- r;
        }
        q[++r] = i;
        while(l < r && crs(q[l], q[l+1]) <= i){
            ++ l;
        }
        p[i] = max(p[i], (int)ceil(fx(q[l], i)) - a[i]);
    }
    reverse(p + 1, p + n + 1);
    for(int i = 1; i <= n; ++ i){
        printf("%d\n", p[i]);
    }
    return 0;
}

34. JLOI2015 - 战争调度 [省选/NOI-]

首先有一个暴力的做法:从左往右扫所有的自根向叶的链上选择的状态 \(S\),设 \(f_{i,j,S}\) 表示前 \(i\) 个叶子,到根路径状态为 \(S\),选了 \(j\) 个战争叶子的最优解,容易发现复杂度至少是 \(O(8^n)\) 的,过不去。

还有一个做法是:dfs 遍历每个点的同时记录根到点路径的选择方案,遍历到叶子的时候根据选择方案算出叶子的答案,回溯的时候使用 dp:\(f_{x,i+j}=\max\{f_{x<<1,i}+f_{x<<1|1,j}\}\) 更新各个节点的答案。这个的复杂度是 \(\sum_{i=1}^n2^{i-1}(2^{n-i-1})^22^{i-1}+2^{n-1}2^{n-1}n=O(n4^n)\) 的,可以通过。

这两个做法看起来很相似,但是为什么复杂度不同?或者说第一个做法多计算了哪些部分?

实际上,对于根的两棵子树 \(2,3\)\(2\) 最右侧的链的状态对于 \(3\) 是完全没有影响的。统计一下发现多记录的状态和为 \(O(2^{n+1})\)

点击查看代码
const int N = 1030;
int n, m, zz[N][13], hq[N][13], f[N][N];

void dfs(int dep, int x, int state){
	if(dep == n){
		int ZZ = 0, HQ = 0, p = x - (1 << n - 1) + 1;
		for(int i = 1; i < n; ++ i){
			if(state & (1 << i)){
				ZZ += zz[p][n-i];
			} else {
				HQ += hq[p][n-i];
			}
		}
		f[x][0] = HQ;
		f[x][1] = ZZ;
	} else {
		int siz = 1 << (n - dep - 1);
		for(int i = 0; i <= siz * 2; ++ i){
			f[x][i] = 0;
		}
		dfs(dep + 1, x << 1, state);
		dfs(dep + 1, x << 1 | 1, state);
		for(int i = 0; i <= siz; ++ i){
			for(int j = 0; j <= siz; ++ j){
				f[x][i+j] = max(f[x][i+j], f[x<<1][i] + f[x<<1|1][j]);
			}
		}
		dfs(dep + 1, x << 1, state | (1 << dep));
		dfs(dep + 1, x << 1 | 1, state | (1 << dep));
		for(int i = 0; i <= siz; ++ i){
			for(int j = 0; j <= siz; ++ j){
				f[x][i+j] = max(f[x][i+j], f[x<<1][i] + f[x<<1|1][j]);
			}
		}
	}
}

void solve(){
	read(n, m);
	for(int i = 1; i <= (1 << n-1); ++ i){
		for(int j = 1; j < n; ++ j){
			read(zz[i][j]);
		}
	}
	for(int i = 1; i <= (1 << n-1); ++ i){
		for(int j = 1; j < n; ++ j){
			read(hq[i][j]);
		}
	}
	dfs(1, 1, 0);
	int ans = 0;
	for(int i = 0; i <= m; ++ i){
		ans = max(ans, f[1][i]);
	}
	println(ans);
}

posted @ 2024-02-06 10:30  KiharaTouma  阅读(53)  评论(1编辑  收藏  举报