CCF统一省选 Day2 题解

此题解是教练给我的作业,AK了本场比赛的人,以及认为题目简单的人可以不必看

T1

算法一

暴力枚举对信号站顺序的不同排列,然后对代价取\(\min\)即可。
时间复杂度\(O(m! \cdot n)\),可以获得\(30\)分。

算法二

首先我们的想法是状压dp,而状压dp所记录的状态是某个位置前面所选择的信号站集合以及当前加入的信号站编号,我们需要把答案的每一项的贡献分配到dp的不同阶段!

我们设最终的排列里面编号为\(i\)的信号站在第\(p_i\)个位置。

对于\(1 \leq i < n,S_i \neq S_{i + 1}\),我们若\(p_{S_i} < p_{S_{i + 1}}\),则对答案的贡献为\(p_{S_{i + 1}} - p_{S_i}\)。我们在按位置顺序加入\(S_i\)的过程中,由于我们“知道“了\(S_{i + 1}\)不在前面,所以就给它减去\(p_{S_i}\)的贡献。在加入\(S_{i + 1}\)的过程中,由于我们知道\(S_i\)在前面,所以就给它加上\(p_{S_{i + 1}}\)的贡献。

反之若\(p_{S_i} > p_{S_{i + 1}}\),我们在加入\(S_i\)的过程中,知道了\(S_{i + 1}\)在前面,就加上\(kp_{S_{i}}\)的贡献。在加入\(S_{i + 1}\)时,知道了\(S_i\)不在前面,就加上\(kp_{S_{i}}\)的贡献。

所以我们可以预处理二维数组\(c_{i, j}, d_{i, j}\),分别表示在加入\(i\)的过程中若\(j\)在前面,那么贡献的系数是多少。以及若\(j\)在前面,贡献的系数是多少。

写出初值和转移方程(实现时改从\(0\)编号):\(f_{\emptyset} = 0, f_{S} = \min_{i \in S} {f_{S - \{ i \}} + (\sum_{j \in S} c_{i, j} + \sum_{j \not \in S} d_{i, j}) \cdot \lvert S \rvert}\)

直接实现它,可以获得\(60\)分,时间复杂度\(O(n + 2^m \cdot m^2)\)

算法三

考虑优化复杂度和常数。对\(i \in S\),记\(\sum_{j \in S} c_{i, j} + \sum_{j \not \in S} d_{i, j} = e_{S, i}\)。在\(S\)的编号从\(1\)\(2^{m} - 1\)依次增大的时候,例如\(S\)编号从\(p - 1\)\(p\)\(e_{S,j}\)的变化相当于是\(p\)的lowbit之前的一个前缀的贡献由\(d\)变成了\(c\),以及\(lowbit\)这一位发生的变化。

因此我们对每个\(i\)预处理\(d_{i, j} - c_{i, j}\)的前缀和,在\(S\)变化的时候动态更改\(e_{S, i}\)(这里不需要记录\(S\)\(S\)看作时间),就可以用极少的额外空间把时间复杂度优化到\(O(n + 2^m \cdot m)\)了!可以获得\(100\)分。若常数比较大,只能获得\(70\)\(90\)分。

代码实现

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

const int N = 100005, M = 23;
const int inf = 1500000000;

template <class T>
void read (T &x) {
	int sgn = 1;
	char ch;
	x = 0;
	for (ch = getchar(); (ch < '0' || ch > '9') && ch != '-'; ch = getchar()) ;
	if (ch == '-') ch = getchar(), sgn = -1;
	for (; '0' <= ch && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
	x *= sgn;
}
template <class T>
void write (T x) {
	if (x < 0) putchar('-'), write(-x);
	else if (x < 10) putchar(x + '0');
	else write(x / 10), putchar(x % 10 + '0');
}

int n, m, k, s[N];
int cnt[1 << M], lowbit[1 << M], f[1 << M];
int val1[M][M], val2[M][M], pre[M][M + 1], val[M];

int main () {
	freopen("transfer.in", "r", stdin);
	freopen("transfer.out", "w", stdout);
	read(n), read(m), read(k);
	for (int i = 1; i <= n; i++) read(s[i]), s[i]--;
	for (int i = 0; i < m; i++) {
		for (int j = 0; j < m; j++) val1[i][j] = val2[i][j] = 0;
	}
	for (int i = 1; i < n; i++) {
		if (s[i] != s[i + 1]) {
			val1[s[i]][s[i + 1]] += k;
			val2[s[i + 1]][s[i]] += k;
			val1[s[i + 1]][s[i]]++;
			val2[s[i]][s[i + 1]]--;
		}
	}
	
	int U = (1 << m) - 1;
	f[0] = lowbit[0] = cnt[0] = 0;
	for (int i = 1; i <= U; i++) {
		f[i] = inf;
		cnt[i] = cnt[i >> 1] + (i & 1);
		lowbit[i] = (i & 1) ? 0 : lowbit[i >> 1] + 1;
	}
	for (int i = 0; i < m; i++) {
		pre[i][0] = 0;
		for (int j = 0; j < m; j++) {
			val[i] += val2[i][j];
			pre[i][j + 1] = pre[i][j] + val2[i][j] - val1[i][j];
		}
		for (int j = 0; j < m; j++) pre[i][j] += val1[i][j] - val2[i][j];
	}
	for (int i = 1; i <= U; i++) {
		f[i] = inf;
		int low = lowbit[i];
		for (int j = 0, s = 1; j < m; j++, s <<= 1) {
			val[j] += pre[j][low];
			if (i & s) f[i] = min(f[i], f[i ^ s] + val[j] * cnt[i]);
		}
	}
	write(f[U]), putchar('\n');
	fclose(stdin), fclose(stdout);
	return 0;
}

评注

本题是一个考察选手状压dp的基本理解的题目,类似的题目在\(cf\)中出现了。理解状压dp的过程中记录了什么,才有助于对答案的式子进行重新整理,重新转化。

T2

算法一

直接暴力,枚举每棵子树的顶点计算答案即可。时间复杂度为\(O(n^2)\),可以获得\(10\)分。

算法二

由于其它部分分或多或少与正解有一定重合度,我们就直接讲正解了。

询问子树信息可以考虑使用线段树/启发式合并来做(当然用dfs序转换成区间询问是另一种常见方法)。在这里我们可以转换为:对这棵树做自底向上的\(dfs\),假设我们要算\(val_u\),且\(u\)的儿子已经算好。那么我们把\(u\)的儿子的数的可重集合并起来,然后给每个数加1,然后再加入\(u\)上面的数,询问所有在某个容器的数的异或和。

于是我们需要支持合并,支持整体加1,支持加入一个数,以及询问整体的异或和。

“异或和”这个信息迫使我们采用trie树。但传统的从高往低的trie树的优点在于在询问异或和的同时很好的表示了序关系,却不能很好的支持加1操作。所以我们考虑从低位到高位建的trie树。然后我们发现对整棵trie树的加一操作,就是先交换根的左右儿子(因为如果某个数二进制最后一位是\(0\),就变成\(1\),否则变成\(0\))。然后对需要进位的原先的右子树,我们递归地再进行这样的操作!这样的时间复杂度是\(O(\log W)\)(\(W\)为生成的所有的数的上限)

接着合并的操作类似于线段树合并。为了实现查询的操作,你需要在每个节点维护这棵子树里面所有数的异或和,以及这棵子树的大小,就可以做\(pushup\)操作了。

时间复杂度\(O(n \log W)\),可以获得\(100\)分。

注意这里\(W\)的范围可能达到\(2^20\)以上,请确保trie树的深度是足够的。

代码实现

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

const int N = 525015, M = 22;

template <class T>
void read (T &x) {
	int sgn = 1;
	char ch;
	x = 0;
	for (ch = getchar(); (ch < '0' || ch > '9') && ch != '-'; ch = getchar()) ;
	if (ch == '-') ch = getchar(), sgn = -1;
	for (; '0' <= ch && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
	x *= sgn;
}
template <class T>
void write (T x) {
	if (x < 0) putchar('-'), write(-x);
	else if (x < 10) putchar(x + '0');
	else write(x / 10), putchar(x % 10 + '0');
}

struct edge {
	int to, nxt;
} tree[N << 1];
int n, head[N], a[N], maxdep = 0, cnt = 0;
void addedge (int u, int v) {
	edge e = {v, head[u]};
	tree[head[u] = cnt++] = e;
}

int ch[N * M][2], sz[N * M], num[N * M], val[N * M], tot = 0;
int newnode () {
	int rt = tot++;
	ch[rt][0] = ch[rt][1] = -1;
	sz[rt] = val[rt] = 0;
	return rt;
}
void pushup (int now) {
	sz[now] = num[now], val[now] = 0;
	if (~ch[now][0]) {
		sz[now] += sz[ch[now][0]];
		val[now] ^= (val[ch[now][0]] << 1);
	}
	if (~ch[now][1]) {
		sz[now] += sz[ch[now][1]];
		val[now] ^= (val[ch[now][1]] << 1);
		if (sz[ch[now][1]] & 1) val[now] |= 1;
	}
}

int insert (int dep, int now, int x) {
	int rt = now;
	if (rt < 0) rt = newnode();
	if (dep) {
		if (x & 1) ch[rt][1] = insert(dep - 1, ~now ? ch[now][1] : -1, x >> 1);
		else ch[rt][0] = insert(dep - 1, ~now ? ch[now][0] : -1, x >> 1);
	}
	else num[rt]++;
	pushup(rt);
	return rt;
}
void add (int rt) {
	if (~rt) {
		swap(ch[rt][0], ch[rt][1]);
		add(ch[rt][0]);
		pushup(rt);
	}
}
int merge (int u, int v) {
	if (u < 0) return v;
	if (v < 0) return u;
	num[u] += num[v];
	ch[u][0] = merge(ch[u][0], ch[v][0]);
	ch[u][1] = merge(ch[u][1], ch[v][1]);
	pushup(u);
	return u;
}

int root[N];
long long ans = 0ll;
void dfs (int u) {
	root[u] = -1;
	for (int i = head[u]; ~i; i = tree[i].nxt) {
		int v = tree[i].to;
		dfs(v);
		root[u] = merge(root[u], root[v]);
	}
	add(root[u]);
	root[u] = insert(maxdep, root[u], a[u]);
	ans += val[root[u]];
}

int main () {
	freopen("tree.in", "r", stdin);
	freopen("tree.out", "w", stdout);
	read(n);
	for (int i = 1; i <= n; i++) read(a[i]), head[i] = -1;
	for (int i = 2; i <= n; i++) {
		int fa;
		read(fa);
		addedge(fa, i);
	}
	int mx = 0;
	for (int i = 1; i <= n; i++) mx = max(mx, a[i] + n - 1);
	for (int i = 0; (1 << i) <= mx; maxdep = ++i) ;
	dfs(1);
	write(ans), putchar('\n');
	fclose(stdin), fclose(stdout);
	return 0;
}

评注

这道题目的核心想法和AGC044C Strange Dance很相似,见过这个想法的人思考这道题目会有较大的优势。

T3

算法一

暴力枚举每条边是否被选择,然后判断是否是生成树,计算答案即可。

时间复杂度\(O(2^m \cdot (n + \log W))\)(次数\(W\)为最大的权值),可以获得\(10\)分。

算法二

\(m \leq n\)时,若\(m = n - 1\),生成树最多有一棵。若\(m = n\),假定图若连通,那么图\(G\)是基环树。我们找到环,枚举哪一条边不被选择,然后对最多\(n\)棵生成树计算答案再加起来即可。

时间复杂度\(O(n + \log W)\)\(O(n(n + \log W))\)不等。结合算法一可以获得\(30\)分。

算法三

\(w_i\)均相同,则我们只需要算生成树个数,再乘上\(w_1^2(n - 1)\)即可!这里直接用matrix-tree定理计算生成树个数即可。结合算法一、二可以获得\(50\)分。

算法四

这里\(w_i\)均为素数的方法我们不在赘述,因为想出这档部分分和正解一样,都是要考虑如何处理生成树的权值和

首先考虑用莫比乌斯反演。设\(f(i)\)表示权值\(gcd\)\(i\)的生成树中权值和的和,\(g(i)\)表示权值\(gcd\)\(i\)整除的生成树的\(gcd\)的和。
则有\(g(i) = \sum_{i \vert j} f(j)\)。只要求出了\(g\),我们就可以求出\(f\),进而算出答案。

对单个\(g(i)\),我们把每条权值被\(i\)整除的边拿出来,只需要求这个子图的生成树的权值和的和。对每条边\(i\),我们将它的新的权值视作\(1 + w_ix\)则所有生成树的新的权值的积的一次项系数就是答案!

我们只需要把matrix-tree定理里面的基尔霍夫矩阵的每个元素换成一次多项式,在\(\mod x^2\)意义下计算行列式!考虑模仿高斯消元,只不过把一个数的逆换成了多项式\(\mod x^2\)的逆元。但这里会出现一个问题(虽然不知道最终数据有没有卡这个细节),就是有常数项模998244353余\(0\)时没有逆元。于是我们需要在某一列下面所有多项式常数项都为\(0\)的时候,消掉一个\(x\),做普通的行列式,才可以解决这个小问题。

另一个小问题时时间复杂度。设\(1-W\)的最大的正因子的数量为\(M\),那么我们可能需要做\(mM\)\(O(n^3)\)的行列式,是会TLE的。这里我们发现如果某个图的边数\(<n - 1\),就没有生成树了,不必做行列式。通过简单算两次,我们所做的行列式次数最多为\(\frac{mM}{n - 1}\),可以通过。

时间复杂度\(O(n^2mM)\),可以获得\(100\)分。

代码实现

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

const int N = 35, K = 152505;
const long long mod = 998244353ll;

template <class T>
void read (T &x) {
	int sgn = 1;
	char ch;
	x = 0;
	for (ch = getchar(); (ch < '0' || ch > '9') && ch != '-'; ch = getchar()) ;
	if (ch == '-') ch = getchar(), sgn = -1;
	for (; '0' <= ch && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
	x *= sgn;
}
template <class T>
void write (T x) {
	if (x < 0) putchar('-'), write(-x);
	else if (x < 10) putchar(x + '0');
	else write(x / 10), putchar(x % 10 + '0');
}

int n, m, mx = 0, u[N * N], v[N * N], w[N * N];
vector<int> tmp[K], vec[K];
long long f[K], mat0[N][N], mat1[N][N], ans = 0ll;

long long qpow (long long a, long long b) {
	long long res = 1ll;
	for (; b; b >>= 1, a = a * a % mod) {
		if (b & 1) res = res * a % mod;
	}
	return res;
}
long long solve () {
	bool flag = false;
	long long ans0 = 1ll, ans1 = 0ll;
	for (int i = 1; i < n; i++) {
		int pos0 = 0, pos1 = 0;
		for (int j = i; j < n; j++) {
			if (mat0[j][i]) pos0 = j;
			else if (mat1[j][i]) pos1 = j;
		}
		if (pos0) {
			if (pos0 != i) ans0 = (mod - ans0) % mod, ans1 = (mod - ans1) % mod;
			for (int j = i; j < n; j++) {
				swap(mat0[i][j], mat0[pos0][j]);
				swap(mat1[i][j], mat1[pos0][j]);
			}
			ans1 = (ans0 * mat1[i][i] + ans1 * mat0[i][i]) % mod;
			ans0 = ans0 * mat0[i][i] % mod;
			long long inv0 = qpow(mat0[i][i], mod - 2);
			long long inv1 = (mod - mat1[i][i]) * inv0 % mod * inv0 % mod;
			for (int j = i + 1; j < n; j++) {
				long long coef0 = mat0[j][i] * inv0 % mod;
				long long coef1 = (mat0[j][i] * inv1 + mat1[j][i] * inv0) % mod;
				for (int k = i; k <= n; k++) {
					mat0[j][k] = (mat0[j][k] + mat0[i][k] * (mod - coef0)) % mod;
					mat1[j][k] = (mat1[j][k] + mat0[i][k] * (mod - coef1)) % mod;
					mat1[j][k] = (mat1[j][k] + mat1[i][k] * (mod - coef0)) % mod;
				}
			}
		}
		else if (pos1) {
			if (flag) return 0ll;
			if (pos1 != i) ans0 = (mod - ans0) % mod, ans1 = (mod - ans1) % mod;
			for (int j = i; j < n; j++) mat0[j][i] = mat1[j][i];
			for (int j = i; j < n; j++) swap(mat0[i][j], mat0[pos1][j]);
			ans1 = ans0, flag = true;
			ans1 = ans1 * mat0[i][i] % mod;
			long long inv = qpow(mat0[i][i], mod - 2);
			for (int j = i + 1; j < n; j++) {
				long long coef = mat0[j][i] * inv % mod;
				for (int k = i; k < n; k++) mat0[j][k] = (mat0[j][k] + mat0[i][k] * (mod - coef)) % mod;
			}
		}
		else return 0ll;
	}
	return ans1;
}

int main () {
	freopen("count.in", "r", stdin);
	freopen("count.out", "w", stdout);
	read(n), read(m);
	for (int i = 0; i < m; i++) {
		read(u[i]), read(v[i]), read(w[i]);
		u[i]--, v[i]--;
		mx = max(mx, w[i]);
	}
	for (int i = 1; i <= mx; i++) {
		for (int j = i; j <= mx; j += i) tmp[j].push_back(i);
	}
	
	for (int i = 0; i < m; i++) {
		for (int j = 0; j < tmp[w[i]].size(); j++) {
			vec[tmp[w[i]][j]].push_back(i);
		}
	}
	for (int i = 1; i <= mx; i++) {
		f[i] = 0ll;
		if ((int)vec[i].size() >= n - 1) {
			for (int j = 1; j < n; j++) {
				for (int k = 1; k < n; k++) mat0[j][k] = mat1[j][k] = 0ll;
			}
			for (int j = 0; j < vec[i].size(); j++) {
				int a = u[vec[i][j]], b = v[vec[i][j]], val = w[vec[i][j]];
				if (a && b) {
					mat0[a][a] = (mat0[a][a] + 1ll) % mod;
					mat0[b][b] = (mat0[b][b] + 1ll) % mod;
					mat0[a][b] = (mat0[a][b] + mod - 1ll) % mod;
					mat0[b][a] = (mat0[b][a] + mod - 1ll) % mod;
					mat1[a][a] = (mat1[a][a] + val) % mod;
					mat1[b][b] = (mat1[b][b] + val) % mod;
					mat1[a][b] = (mat1[a][b] + mod - val) % mod;
					mat1[b][a] = (mat1[b][a] + mod - val) % mod;
				}
				else if (a) {
					mat0[a][a] = (mat0[a][a] + 1ll) % mod;
					mat1[a][a] = (mat1[a][a] + val) % mod;
				}
				else {
					mat0[b][b] = (mat0[b][b] + 1ll) % mod;
					mat1[b][b] = (mat1[b][b] + val) % mod;
				}
			}
			f[i] = solve();
		}
	}
	for (int i = mx; i >= 1; i--) {
		for (int j = i << 1; j <= mx; j += i) f[i] = (f[i] + mod - f[j]) % mod;
		ans = (ans + f[i] * i) % mod;
	}
	write(ans), putchar('\n');
	fclose(stdin), fclose(stdout);
	return 0;
}

评注

这道题目是计数的常见套路的集锦,例如如何处理\(gcd\),matrix-tree的简单推广,如何对算法做剪枝等等。

posted @ 2020-06-24 13:48  unzcjouhi  阅读(426)  评论(0编辑  收藏  举报