周作业 42

A - Pull Your Luck

题意

给定 \(n,x,p\),要求选择 \(f \in [1,p]\),使得 \(x\) 在依次增加 \(f,f-1,f-2,\cdots,1\) 之后为 \(n\) 的倍数,求是否求解。

时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define il inline
#define N 100005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
int n, x, p;
signed Main(){
	n = rd(), x = rd(), p = rd();
	for (int s = 1, i = 1; i <= min(p, n + n); i++, s = (s + i % n) % n) if (s == (n - x) % n) return puts("Yes");
	puts("No");
	return 0;
}
signed main(){
	for (int T = rd(); T--;) Main();
	return 0;
}


分析

注意到,当 \(f = 2n\) 时,\(x\) 会加回到 \(x\),因为 \(\frac{2n(2n+1)}{2} = n(2n+1)\)\(n\) 的倍数。所以直接枚举 \(f\) 即可。

B - Shocking Arrangement

题意

有一个序列 \(a\),保证 \(\sum a_i = 0\)。现在你要重排 \(a\),使得满足下面式子。

\[\max_{1\le L \le R \le n} |a_L+a_{L+1}+\cdots+a_{R}| < \max_{i=1}^n a_i - \min_{i=1}^n a_i \]

求方案或报告无解。

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

分析

首先注意到,我们应该让前缀和的最大最小值尽量接近 \(0\),此时是最优的。而无解当且仅当 \(a_i\) 全都是 \(0\)

所以我们可以排序,然后选择取最小值或最大值,如果为负数去最大值,为正数取最小值。

当然,这题做法很多,只要意思接近,做法类似即可。当然也不用排序,只要分成正负数即可。

至于我的做法比较抽象,一样的是取最大最小值,如果放最大值后不合法就放最小值,放最小值不合法就放最大值,都不合法就是无解,都合法就随意。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define il inline
#define N 300005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
const int INF = 1e9;
int n, a[N], mx, mn, s, mxs, mns;
il bool check(int s){return abs(s - mxs) < mx - mn && abs(s - mns) < mx - mn;}
signed Main(){
	n = rd(), mx = -INF, mn = INF;
	multiset <int> S;
	for (int i = 1, x; i <= n; i++) x = rd(), mx = max(mx, x), mn = min(mn, x), S.insert(x);
	if (n == 1) return puts("No"), 0;
	s = 0, mxs = 0, mns = 0; 
	for (int i = 1; i <= n; i++){
		auto itl = S.begin(), itr = --S.end();
		int fl = check(s + *itl), fr = check(s + *itr);
		if (!fl && !fr) return puts("No"), 0;
		if (fl) a[i] = *itl, S.erase(itl);
		else a[i] = *itr, S.erase(itr);
		s += a[i];
		mxs = max(mxs, s), mns = min(mns, s);
	}
	puts("Yes");
	for (int i = 1; i <= n; i++) printf ("%lld ", a[i]);
	puts("");
	return 0;
}
signed main(){
	for (int T = rd(); T--;) Main();
	return 0;
}


C - Climbing the Tree

题意

有一口井,深度为 \(h\)。一开始你并不知道 \(h\) 为多少。一直蜗牛有两个属性 \(a,b\),表示这只蜗牛会在白天向上爬 \(a\),晚上向下掉 \(b\),保证 \(a>b\)

现在有 \(q\) 次操作,有两种。

  • 1 a b n,表示有一只属性为 \(a,b\) 的蜗牛从井底出发,在第 \(n\) 天出井。你需要判断是否可能,若可能,则视为该信息正确,返回 \(1\)。若不可能,返回 \(0\)
  • 2 a b,表示询问一直属性为 \(a,b\) 的蜗牛从井底出发要多少天才能出井,若答案不唯一,返回 \(-1\)

时间复杂度 \(O(q)\)

分析

不难发现一只蜗牛在第 \(n\) 天出井,当且仅当 \(h \in [(a-b)(n-2)+a+1,(a-b)(n-1)+a]\)。前者表示 \(n-1\) 天的最高高度 \(+1\),后者表示 \(n\) 天的最高高度。

我们直接记 \([L,R]\) 表示当且可以确定的 \(h\) 的范围。当给定新的信息时,将新的区间和 \([L,R]\) 比对,若没有交,说明信息是假的,否则就是真的。如果是真的信息,就更新 \([L,R]\) 为其交集。

查询的时候,我们分别计算 \(h = L\)\(h = R\) 时的天数,若不同,就是 \(-1\),否则就是对应天数。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define il inline
#define N 100005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
const int INF = 1e18;
signed Main(){
	int L = 1, R = INF;
	for (int Q = rd(), op, a, b, n; Q--;){
		op = rd(), a = rd(), b = rd();
		if (op == 1){
			n = rd();
			int l = (n == 1 ? 1 : (a - b) * (n - 2) + a + 1), r = (a - b) * (n - 1) + a;
			if (r < L || l > R) printf ("0 ");
			else printf ("1 "), L = max(L, l), R = min(r, R);
		}
		else{
			int hl = max((L - b - 1) / (a - b) + 1, 1ll), hr = max((R - b - 1) / (a - b) + 1, 1ll);
			if (hl == hr) printf ("%lld ", hl);
			else printf ("-1 ");
		}
	}
    puts("");
    return 0;
}
signed main(){
	for (int T = rd(); T--;) Main();
	return 0;
}


D - A Wide, Wide Graph

题意

给定一棵树,定义一张无向图 \(G_k\),生成方式为找到所有在树上距离不小于 \(k\) 的点对,并在 \(G_k\) 中连接。问对于 \(k \in [1,n]\)\(G_k\) 的连通块数量。

时间复杂度 \(O(n)\)

分析

首先会发现,设直径长度为 \(d\),当 \(k > d\) 的时候 \(G_k\) 所有点都是孤立点。

\(k \le d\) 时,显然直径两个端点在同一连通块内,而对于一个点 \(i\),如果它到直径端点最远距离比 \(k\) 小,那么显然这个点就是孤立点,否则这个点与直径相连。

于是我们直接找出直径,然后差分一下即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define N 100005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
int n, d[2][N], s, t, ans[N];
vector <int> e[N];
void dfs(int u, int *d, int fa = 0){for (int v : e[u]) if (v != fa) d[v] = d[u] + 1, dfs(v, d, u);}
signed main(){
	n = rd();
	for (int i = 1, u, v; i < n; i++) u = rd(), v = rd(), e[u].push_back(v), e[v].push_back(u);
	d[1][1] = 0, dfs(1, d[1]);
	for (int i = 1; i <= n; i++) if (d[1][s] < d[1][i]) s = i;
	d[0][s] = 0, dfs(s, d[0]);
	for (int i = 1; i <= n; i++) if (d[0][t] < d[0][i]) t = i;
	d[1][t] = 0, dfs(t, d[1]);
	for (int i = 1; i <= n; i++) if (i != s) ans[max(d[0][i], d[1][i]) + 1]++;
	ans[1] = 1;
	for (int i = 2; i <= n; i++) ans[i] += ans[i - 1];
	for (int i = 1; i <= n; i++) printf ("%d ", ans[i]);
	return 0;
}


E - No Cost Too Great (Easy Version)

Easy Version 和 Hard Version 是字面意思,E 题真的简单,F 真的有点难。

题意

给定一个序列 \(a\),一次操作可以令一个 \(a_i\)\(1\),问至少几次操作后 \(a\) 中至少有两个数不互质。

时间复杂度 \(O(n\log{V})\)

分析

可以注意到,答案不超过 \(2\),最坏情况就是将两个奇数加 \(1\)。考虑计算答案为 \(0\)\(1\) 的。

对于答案为 \(0\),就是计算是否有两个数不互质,可以预处理所有数拥有的质因数,然后用桶存一下即可。

对于答案为 \(1\),就是计算是否有一个数加 \(1\) 之后和别的数不互质,一样用桶即可。

但是我手比脑子快,没有筛质因数,直接筛的质数,时间复杂度会劣一些,但是也能过。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define N 200005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
int n, a[N], cnt[N];
vector <int> p[N];
signed Main(){
	n = rd();
	int flg = 0;
	for (int i = 1; i <= n; i++){
		a[i] = rd();
		for (int j : p[a[i]]){
			if (j != 1 && cnt[j]) flg = 1;
			cnt[j]++;
		}
	}
	for (int i = 1; i <= n; i++) rd();
	if (flg){
		for (int i = 1; i <= n; i++) for (int j : p[a[i]]) cnt[j]--;
		return puts("0"), 0;
	}
	int flg1 = 0;
	for (int i = 1; i <= n; i++) for (int j : p[a[i] + 1]) if (j != 1 && cnt[j]) flg1 = 1;
	for (int i = 1; i <= n; i++) for (int j : p[a[i]]) cnt[j]--;
	return puts(flg1 ? "1" : "2"), 0;
}
signed main(){
	for (int i = 1; i <= 200001; i++){
		for (int j = 1; j * j <= i; j++) if (i % j == 0){
			p[i].push_back(j);
			if (j * j != i) p[i].push_back(i / j);
		}
		sort (p[i].begin(), p[i].end());
	}
	for (int T = rd(); T--;) Main();
	return 0;
}


F - No Cost Too Great (Hard Version)

题意

给定一个序列 \(a\),一次操作可以令一个 \(a_i\)\(1\)要花费 \(b_i\) 的代价,问至少花费多少代价使得 \(a\) 中至少有两个数不互质。

时间复杂度 \(O(n\log{V})\)

分析

同上的考虑,首先排除答案为 \(0\) 的,然后会发现,答案不超过 \(b_i\) 的最小值加次小值,这种情况是操作两个数。

但是这一次只操作一个数的话,不一定只加一次,因为会有下面的数据。

2
4 9
1 1000000000

这种情况显然令 \(a_1\)\(2\) 最优。但是我们一样可以注意到一个点,就是操作不只 \(1\) 次的位置,\(b_i\) 一定是最小的。所以我们可以枚举质因数,然后计算要花费的代价。而只操作 \(1\) 次的话,可以直接计算。

那会不会有多个最小值呢?注意到如果不只一个最小值,那么最小值一定等于次小值,那么操作多次答案一定不优于操作两个数。于是我们就做完了。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define il inline
#define N 400005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
int n, a[N], b[N], cnt[N], pr[N], tot, isp[N], mxp[N];
vector <int> p[N];
signed Main(){
	n = rd();
	int flg = 0;
	for (int i = 1; i <= n; i++){
		a[i] = rd();
		for (int j : p[a[i]]){
			if (cnt[j]) flg = 1;
			cnt[j]++;
		}
	}
	for (int i = 1; i <= n; i++) b[i] = rd();
	if (flg){
		for (int i = 1; i <= n; i++) for (int j : p[a[i]]) cnt[j]--;
		return puts("0"), 0;
	}
	int ans = 2e9, mn = 2e9, cmn = 2e9;
	for (int i = 1; i <= n; i++) if (b[i] <= mn) cmn = mn, mn = b[i]; else if (b[i] < cmn) cmn = b[i];
	ans = mn + cmn;
	for (int i = 1; i <= n; i++) for (int j : p[a[i] + 1]) if (cnt[j]) ans = min(ans, b[i]);
	if (mn < cmn){
		int pos = 0;
		for (int i = 1; i <= n; i++) if (b[i] == mn) pos = i;
		for (int i = 1; i <= n; i++) if (i != pos) for (int j : p[a[i]])
			ans = min(ans, (j - a[pos] % j) % j * mn);
	}
	for (int i = 1; i <= n; i++) for (int j : p[a[i]]) cnt[j]--;
	printf ("%lld\n", ans);
	return 0;
}
signed main(){
	for (int i = 2; i <= 400000; i++) if (!isp[i]){
		pr[++tot] = i;
		for (int j = i + i; j <= 400000; j += i) isp[j] = 1;
	}
	for (int i = 1; i <= 400000; i++){
		int x = i;
		for (int j = 1; j <= tot && pr[j] * pr[j] <= x; j++) if (x % pr[j] == 0){
			p[i].push_back(pr[j]);
			while (x % pr[j] == 0) x /= pr[j];
		}
		if (x > 1) p[i].push_back(x), mxp[i] = x;
	}
	for (int T = rd(); T--;) Main();
	return 0;
}


G - Accommodation

题意

给定一个 \(n \times m\)\(01\) 网格,你要在每行放置 \(\frac{m}{2}\)\(1 \times 1\) 的骨牌和 \(\frac{m}{4}\)\(1 \times 2\) 的横向骨牌,保证 \(m\)\(4\) 的倍数。定义一个放置方案的权值为有覆盖到 \(1\) 的骨牌数量,问最小和最大权值分别是多少。

时间复杂度 \(O(nm)\)

分析

显然每行独立,分别考虑。如果全是 \(1 \times 1\) 的或者 \(1 \times 2\) 的至多覆盖一个 \(1\),那么权值就是 \(\sum a_{i,j}\),即 \(1\) 的数量。但是有一些会覆盖 \(2\)\(1\) 的骨牌,我们要减去,求的就是最多和最少有几个 \(1 \times 2\) 的骨牌覆盖了两个 \(1\)

如果要覆盖两个最大的话,直接找出极长连续的 \(1\) 段,然后把它长度除以 \(2\) 的值加起来。

找最少的话,也可以贪心,但是我不会,选择直接 dp,设 \(f_{i,0/1}\) 表示前 \(i\) 位,第 \(i\) 位是否选的最小值,转移就是简单的了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define N 500005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
const int INF = 1e9;
int n, m, a[N], sum, s1, s2, f[N][2];
char ch[N];
signed main(){
	m = rd(), n = rd();
	for (int c = 1; c <= m; c++){
		scanf ("%s", ch + 1);
		for (int i = 1; i <= n; i++) a[i] = ch[i] - '0', sum += a[i];
		int ct = 0, len = 0;
		for (int i = 1; i <= n; i++){
			if (a[i]) len++;
			else ct += len / 2, len = 0;
		}
		s1 += min(n / 4, ct + len / 2);
		
		f[0][0] = -INF, f[0][1] = 0;
		for (int i = 1; i <= n; i++) f[i][0] = max(f[i - 1][0], f[i - 1][1]), f[i][1] = f[i - 1][0] + !(a[i] & a[i - 1]);
		s2 += max(n / 4 - max(f[n][0], f[n][1]), 0); 
	}
	printf ("%d %d", sum - s1, sum - s2);
	return 0;
}


H - Monsters

题意

有一张 \(n\) 个点 \(m\) 条边的无向图。你有一个战斗力 \(x\),初始为 \(0\),每个点都有一只怪兽,战力为 \(a_i\)。初始你可以选择一个 \(a_i = 0\) 的点,并击败该怪兽。每击败一只怪兽,\(x\)\(1\),你每次只能击败当前以击败的怪兽相邻的怪兽,且 \(a_i \le x\)。问最后是否可以击败所有怪兽。

时间复杂度 \(O(m\log m)\)

分析

首先不难想到要先建重构树,当你能走到重构树的某一个点时,你就可以击败该点子树内所有点。于是建重构树,然后判断是否存在一条从 \(a_i=0\) 到根节点的路径,满足路径上所有所有父子对 \((u,v)\) 满足 \(a_u \le sz_v\),直接 dfs 即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define N 400005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
int n, m, a[N], fa[N], sz[N], tag[N], tot;
vector <int> e[N];
il int find(int x){return x == fa[x] ? x : fa[x] = find(fa[x]);}
struct Edge{int u, v, w;}ed[N];
bool dfs(int u){
	if (e[u].empty()) return !a[u];
	int flg = 0;
	for (int v : e[u]) if (sz[v] >= a[u]) flg |= dfs(v);
	return flg;
}
signed Main(){
	n = rd(), m = rd(), tot = n;
	for (int i = 1; i <= n + n; i++) fa[i] = i, sz[i] = (i <= n), tag[i] = 0, e[i].clear();
	for (int i = 1; i <= n; i++) a[i] = rd(), tag[i] = (!a[i]);
	for (int i = 1, u, v; i <= m; i++) u = rd(), v = rd(), ed[i] = Edge{u, v, max(a[u], a[v])};
	sort (ed + 1, ed + m + 1, [](Edge x, Edge y){return x.w < y.w;});
	for (int i = 1; i <= m; i++){
		int u = find(ed[i].u), v = find(ed[i].v), w = ed[i].w, mx = 0;
		if (u == v) continue;
		sz[++tot] = sz[u] + sz[v], a[tot] = w, fa[u] = fa[v] = tot, e[tot].push_back(u), e[tot].push_back(v);
	}
	if (sz[tot] < n) return puts("No"), 0;
	puts(dfs(tot) ? "Yes" : "No");
	return 0;
}
signed main(){
	for (int T = rd(); T--;) Main();
	return 0;
}

I - Skate

出到我做过的题了。

题意

有一个 \(n \times m\) 的网格,从 \((i,j)\) 出发时选择一个方向,然后一直向该方向移动直到碰到墙或 #,问要使得从任意一个格子出发可以到达任意格子,至少添加几个 #

分析

注意到从每个格子出发,一定可以到达 \((1,1)\) 以及其他三个角落。所以我们只要判断是否可以从 \((1,1)\) 出发走到每个格子即可。

我们会发现,这个问题的答案不会超过 \(\min(n,m)-2\)。因为最劣情况就是样例一,只要在一行或一列的一边除了角落摆满即可。这启示我们从行列的角度考虑。

我们注意到,只要我们可以走到一行,我们就可以通过这一行上的所有地面走到其他列上去,列也同理,可以走到行上去。所以我们可以访问的格子是类似于行列覆盖的。所以我们考虑一行或一列是否可以走到。

如果把每一行和每一列看作一个点,那么位于 \((r,c)\) 的地面相当于串联起了第 \(r\) 行和第 \(c\) 列,是他们两个只要有一个可达,另一个就一定可达。这十分像合并操作。所以我们考虑将所有互相可达的行和列连起来,用并查集去维护。

我们可以先将四条边连起来,然后枚举每一个地板,连同第 \(r\) 行和第 \(c\) 列。

最后的答案考虑只要所有行或所有列都互相可达即可。所以答案就是行的连通块数量和列的连通块数量的较小值。

时间复杂度 \(O(nm \times \alpha(nm))\)

#include <bits/stdc++.h>
using namespace std;
#define ll int
#define il inline
#define N 1005
#define mg(u, v) fu = find(u), fv = find(v), sz[fu] > sz[fv] ? (sz[fu] += sz[fv], fa[fv] = fu) : (sz[fv] += sz[fu], fa[fu] = fv)
il ll rd(){
	ll s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
ll n = rd(), m = rd(), fu, fv, sz[N << 1], fa[N << 1], t1[N << 1], t2[N << 1], c1, c2;
il ll find(ll x){return x == fa[x] ? x : fa[x] = find(fa[x]);}
char s[N][N];
int main(){
	for (int i = 1; i <= n; i++) scanf ("%s", s[i] + 1);
	for (int i = 1; i <= n + m; i++) fa[i] = i, sz[i] = 1;
	mg(1, n), mg(1, n + 1), mg(1, n + m);
	for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (s[i][j] == '#') mg(i, n + j);
	for (int i = 1; i <= n; i++) if (!t1[find(i)]) c1++, t1[find(i)] = 1;
	for (int i = n + 1; i <= n + m; i++) if (!t2[find(i)]) c2++, t2[find(i)] = 1; 
	printf ("%d\n", min(c1, c2) - 1);
	return 0;
}

J - Multitest Generator

题意

定义一个长度为 \(m\) 的序列 \(a\)正确的,当且仅当 \(a_1 = m - 1\)

定义一个长度为 \(n\) 的序列 \(a\)合法的,当且仅当存在一种将 \([2,n]\) 划分成 \(a_1\) 个区间的方法,使得每一段都是正确的

问对于所有的 \(i \in [1,n-1]\),至少修改多少个位置,使得 \([i,n]\) 的区间是合法的修改后的值可以为 \(0\),但不能为负数

时间复杂度 \(O(n)\)

分析

注意到最坏情况下可以 \(2\) 次操作完,分别修改第 \(1\)\(2\) 个数。而对于不操作,也是好判断的,主要在于怎么判断一个位置能否一次操作完。

我们可以把序列看作一颗内向树,\(i\) 指向 \(i+a_i+1\),而不需要操作就是 \(a_i = dep_{i+1}\),操作一次有两种情况。

  • 修改 \(a_i\),使得 \(a_i = dep_{i+1}\),这种情况当且仅当 \(i+1\) 的根节点是 \(n + 1\)
  • 修改 \(i+1\) 到根节点路径上一个节点,即将这个节点连接到编号比它大的节点中的一个,并使得连接后 \(a_i = dep_{i+1}\)

第一种情况直接判,第二种情况会注意到可以选择的深度是一个左端点为 \(1\) 的区间,所以只要 dp 预处理最大值即可。剩下情况就是答案为 \(2\)

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define N 300005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');	
	return s * w;
}
const int INF = 1e9;
int n, a[N], cnt, dep[N], mx[N], f[N];
signed Main(){
	n = rd(), dep[n + 1] = mx[n + 1] = f[n + 1] = 0;
	for (int i = 1; i <= n; i++) a[i] = rd();
	for (int i = n; i >= 1; i--) if (i + a[i] <= n) dep[i] = dep[i + a[i] + 1] + 1; else dep[i] = -INF;
	for (int i = n; i >= 1; i--) mx[i] = max(mx[i + 1], dep[i]);
	for (int i = n; i >= 1; i--){
		f[i] = mx[i] - dep[i];
		if (i + a[i] + 1 <= n) f[i] = max(f[i], f[i + a[i] + 1]);
	}
	for (int i = 1; i < n; i++){
		int ans = 2;
		if (dep[i + 1] == a[i]) ans = 0;
		else if (dep[i + 1] > 0 || a[i] - dep[i + 1] - 1 <= f[i + 1]) ans = 1;
		printf ("%d ", ans);
	}
	puts("");
	return 0;
}
signed main(){
	for (int T = rd(); T--;) Main();
	return 0;
}


K - DSU Master

出题人并查集大师,但是语文不好。

题意

给定一个长度为 \(n - 1\)\(01\) 序列 \(a\),定义一个长度为 \(k\) 的排列 \(p\) 的权值如下。

  • 以此取出图上 \(p_i\)\(p_{i+1}\) 所在并查集的根节点 \(u,v\)
  • \(a_{p_i} = 0\),将 \(u\) 合并到 \(v\) 上;否则,将 \(v\) 合并到 \(u\) 上。
  • 权值为最后并查集的儿子个数。

求对于所有的 \(k \in [1,n-1]\),所有长度为 \(k\) 的排列的权值和。

时间复杂度 \(O(n)\)

分析

很好的计数题,但是脑子坏掉了,没有想出来。

首先不难想到的是拆贡献,考虑 \(i\)\(1\) 产生贡献的方案数。\(i\) 能对 \(1\) 产生贡献,即 \(i\)\(1\) 的儿子,有三个条件。

  • \(a_{i-1}=0\)
  • \([1,i-1]\) 均在 \(i\) 之前出现。
  • \(1\)\([1,i-1]\) 节点的根节点。

条件 \(1,2\) 好处理,条件 \(3\) 先搁着,看看答案怎么求。

\(k\) 个点的答案是 \(g_k\),将 \([1,i]\) 排列后 \(1\) 为根的方案数为 \(f_i\),那么我们有下面式子。

\[g_k = \sum_{i=1}^{k-1} [a_i = 0]\binom{k-1}{i}(k-i-1)!f_i \]

这个式子表示枚举 \(i\),然后将这 \(i\) 个数放入排列中,剩下的数显然随意排,这 \(i\) 个数有 \(f_i\) 种排法。

考虑拆一下组合数的式子,得到下面的式子。

\[g_k = (k-1)!\sum_{i=1}^{k-1} [a_i=0]\frac{f_i}{i!} \]

显然只要我们知道了 \(f\),就可以 \(O(n)\) 求解。考虑怎么求 \(f\)

本质和上面相同,考虑到这样一个过程应该是先把 \([1,j]\) 并成以 \(1\) 为根,然后 \([j+1,i]\) 乱排,最后用 \(j\) 连起来。

\[f_i = \sum_{j=1}^{i-1} [a_j=0]\binom{i-2}{j-1}(j-i-1)!f_j \]

同上可化简为下面式子。

\[f_i = (i-2)!\sum_{j=1}^{i-1} [a_j=0]\frac{f_j}{j-1} \]

初始状态 \(f_1 = 1\)\(O(n)\) 求即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define N 500005
il int rd(){
	int s = 0, w = 1;
	char ch = getchar();
	for (;ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (;ch >= '0' && ch <= '9'; ch = getchar()) s = ((s << 1) + (s << 3) + ch - '0');
	return s * w;
}
const int P = 998244353;
il int ksm(int x, int r){
	int ans = 1;
	for (; r; x = 1ll * x * x % P, r >>= 1) if (r & 1) ans = 1ll * ans * x % P;
	return ans;
}
int n, s, a[N], f[N], g[N], fact[N], inft[N];
il int C(int n, int m){return n < m || m < 0 ? 0 : 1ll * fact[n] * inft[m] % P * inft[n - m] % P;}
signed Main(){
	n = rd();
	for (int i = 1; i < n; i++) a[i] = rd();
	f[1] = 1, s = (a[1] == 0);
	for (int i = 2; i <= n; i++){
		f[i] = 1ll * fact[i - 2] * s % P;
		if (a[i] == 0) s = (s + 1ll * f[i] * inft[i - 1] % P) % P;
	}
	s = 0;
	for (int i = 1; i <= n; i++){
		g[i] = 1ll * fact[i - 1] * s % P;
		if (a[i] == 0) s = (s + 1ll * f[i] * inft[i] % P) % P;
	}
	for (int i = 2; i <= n; i++) printf ("%d ", g[i]);
	puts("");
	return 0;
}
signed main(){
	fact[0] = 1;
	for (int i = 1; i <= 500000; i++) fact[i] = 1ll * fact[i - 1] * i % P;
	inft[500000] = ksm(fact[500000], P - 2);
	for (int i = 500000; i >= 1; i--) inft[i - 1] = 1ll * inft[i] * i % P;
	for (int T = rd(); T--;) Main();
	return 0;
}


posted @ 2025-11-17 20:05  Union_Find  阅读(3)  评论(0)    收藏  举报