此题解是教练给我的作业,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的简单推广,如何对算法做剪枝等等。

CCF统一省选 Day2 题解的更多相关文章

  1. NOIP2013 DAY2题解

    DAY2 T1积木大赛 传送门 题目大意:每次可以选区间[l,r]加1,最少选几次,让每个位置有 它应有的高度. 题解:O(n)扫一遍就好了.后一个比前一个的高度低,那么前一个已经把它覆盖了, 如果高 ...

  2. JSOI2015 一轮省选 个人题解与小结

    T1: 题目大意:现有一个以1为根节点的树,要求从1开始出发,经过下面的点然后最终要回到根节点.同时除了根节点之外各点均有一个权值(即受益,每个点上的收益只能拿一次,且经过的话必须拿),同时除了根节点 ...

  3. 洛谷P1036 选数 题解 简单搜索/简单状态压缩枚举

    题目链接:https://www.luogu.com.cn/problem/P1036 题目描述 已知 \(n\) 个整数 \(x_1,x_2,-,x_n\) ,以及 \(1\) 个整数 \(k(k& ...

  4. 二模Day2题解

    小明搬家 题目描述 小明要搬家了,大家都来帮忙. 小明现在住在第N楼,总共K个人要把X个大箱子搬上N楼. 最开始X个箱子都在1楼,但是经过一段混乱的搬运已经乱掉了.最后大家发现这样混乱地搬运过程效率太 ...

  5. 【NOIP2012】DAY1+DAY2题解

    不贴代码的原因是我的代码在初中机房.忘记带过来了. DAY 1 T1随便搞,但是字符串相关的题我经常犯蠢 T2 一个结论题,OAO但是需要高精度写. 具体就是按左手的数除右手的数(还是怎么的来着)排个 ...

  6. NOIP[2015] Day2题解

    问题 A: 跳石头 时间限制: 1 Sec  内存限制: 128 MB 题目描述 一年一度的"跳石头"比赛又要开始了! 这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石. ...

  7. 洛古P1036 选数 题解

    [我是传送门] 这是一道很经典的深搜与回溯(难度一般) 可是就这个"普及-" 让本蒟蒻做了一晚上+半个上午(实际我不会深搜回溯,全靠框架+去重); 下面让我分享下本蒟蒻的(全排列+ ...

  8. BZOJ3930:[CQOI2015]选数——题解

    http://www.lydsy.com/JudgeOnline/problem.php?id=3930 https://www.luogu.org/problemnew/show/P3172#sub ...

  9. 【NOIP2014】DAY2题解+代码

    T1 傻逼题……不想写贴昨年代码了. 总之随便怎么搞都能过. 15年的DAY2T1怎么那么毒瘤真是越活越倒退] #include <iostream> #include <fstre ...

随机推荐

  1. 聊一聊sockmap 以及ebpf 实例演示

    eBPF实质上是一个内核注入技术 用户态可以用C来写运行的代码,再通过一个Clang&LLVM的编译器将C代码编译成BPF目标码: 用户态通过系统调用bpf()将BPF目标码注入到内核当中,并 ...

  2. 西数WD2T硬盘分区对齐的方法

    新购一个西数2T硬盘,也就是绿盘的那种,淘宝500左右,支持高级格式化. 到手以后,分区格式化,前几天格式化完成以后,fdisk -l 发现如下文字 引用 Partition 1 does not s ...

  3. Python_faker (伪装者)创建假数据

    faker (伪装者)创建假数据 工作中,有时候我们需要伪造一些假数据,如何使用 Python 伪造这些看起来一点也不假的假数据呢? Python 有一个包叫 Faker,使用它可以轻易地伪造姓名.地 ...

  4. Elementary OS安装及开发环境配置(一)

    前言 假期在家无聊,刚好把六年前的一台笔记本电脑利用起来,原来电脑虽然说配置说不上古董机器,但是运行win系统感觉还是不流畅,所幸给换成Linux桌面版系统,在网上查阅了很多,Linux桌面系统要么推 ...

  5. Unity CommandBuffer物体轮廓

    1.command buffer具有很高的灵活性,它的作用是预定义一些渲染指令,然后在我们想要执行的时候去执行这些指令(见图1),绿点表示可以在"Forward Rendering Path ...

  6. 卷积神经网络图像纹理合成 Texture Synthesis Using Convolutional Neural Networks

    代码实现 概述 这是关于Texture Synthesis Using Convolutional Neural Networks论文的tensorflow2.0代码实现,使用keras预训练的VGG ...

  7. elasticsearch快速安装启动

    准备 docker docker内安装centos容器,模拟服务器环境 centos容器安装 下载centos容器 docker pull centos 启动docker容器 docker run - ...

  8. iOS Transform坐标变化

    在使用CGContext时,由于Quartz 2D与UIKit坐标不一致,所以需要对context进行再一次的变化,达到预期的效果. 1. 不同坐标原点介绍 在Quartz 2D中,坐标原点在画布的左 ...

  9. 攻防世界app2 frida获取密钥

    环境准备 安装mumu模拟器 pip安装frida,这里到最后一步setup需要很长时间. 在frida github下载对应服务端. apk下载:https://adworld.xctf.org.c ...

  10. 深度分析:Java虚拟机类加载机制、过程与类加载器

    虚拟机类加载机制是把描述类的数据从 Class 文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型. ​ 需要注意的是 Java 语言与其他编译时需要进 ...