E https://codeforces.com/contest/1709/problem/E
给你个一个树, 每个点上有值, 修改尽量少的点为任意值
让任何简单路径的 xor都不为零
范围 n 2e5
ai [1,2^30]
3s
256MB
题解 我的思路 显然啊, 类似树上差分的思想, a…b xor = 0
意味着 a…root ^ root …b = 0
所以直接变成 计算每个数到根的xor
然后问题变成, 任意两个相同值之间的连线, 需要至少选一个点
或者说, 需要xor树最终两两不等
不知道往后怎么搞
一个思路是dp
但是 遇到同值点不是 祖先关系,而是分叉的关系 ,也不知打怎么记录
题解 虽然的确有前缀意义,但其实 我想的总思路没问题, 但是细节有问题
并不是简单路径 = 到根的 路径 xor
因为它们的lca 被算了两次
所以其实是 path[u]^path[v] = a[lca[u,v]]
那么 对于一个点x = lca(u,v) , 且 u..v 的xor = 0
那么 (x,y) 上至少有一点需要改变
然后 我们对所有的有这种情况的x 按深度从深到浅考虑
找深度最深的x, 那么修改 x的方案不会比修改 u…v 的更差
因为是最深的, 所以其它满足的链 如果经过它的子树 则必定经过它,因为它需要修改, 所以而至少一个, 而对于其它经过它的链来说是至多一个
dfs(u){ 对于u的所有子树 dfs(v)
每个点获取 从根到它的子节点 能得到的 xor 集合
一旦有来自两个子树 中有值 xor == u 那么u就必然被选 而被选的u对于它的父节点的返回是 空集合 未被选的u对于它的父节点的返回是它所有子集合可达值 xor 它自身 }
怎么看都是n^2 啊 , 为啥 O(nlog^2 n)
因为虽然dfs一次就可以把所有点到根的xor 算完
但是每次做 set 的合并时, 一个点就是会在它的 父节点 中被运算
那每个点被参与比较的次数可以达到 链的长度 不就是 n^2 吗
然后神奇的是 这个启发式合并的复杂度不是n方
启发式合并(DSU on Tree) oi-wiki 树上启发式合并
重儿子, 儿子节点子节点个数最多的
重边, 当前点连向其中一个重儿子的边
轻边 非重边
根到任意点 轻边不超过log n 条
反证法, 如果大于log n 条,
则最深的轻边的根至少还额外连了一个点大小为1的子树
第二深的轻边的根至少还额外连了一个点大小为3的子树
第三深的轻边的根至少还额外连了一个点大小为7的子树
…
第m深的轻边的根至少还额外连了一个点大小为2^m-1的子树
显然 m > log n , 则 2^m - 1 > n - 1 >= n
性质得证
那么每个节点 只有在它的某个祖先u的视角, 它是u的轻边的那一部分时, 才会作为被遍历的
所以每个节点的被遍历次数 = 它到根的 轻边数
所以 O(n log n)
代码 https://codeforces.com/contest/1709/submission/165472602
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <bits/stdc++.h> using namespace std;typedef long long ll;#define rep(i,a,n) for (ll i=a;i<(ll)n;i++) #define pb push_back ll read () {ll r;scanf ("%lld" ,&r);return r;} const int N = 200000 ;int ans=0 ;int a[N+10 ]; int p[N+10 ]; vector<int > e[N+10 ]; set<int > st[N+10 ]; void dfs (int u,int f, int x) { int &iu = p[u]; int bu = x^a[u]; st[iu].insert (bu); bool delu = false ; for (auto v:e[u]) if (v != f){ dfs (v,u,bu); int &iv = p[v]; if (st[iu].size () < st[iv].size ()) swap (iu, iv); for (auto val:st[iv]){ if (delu) break ; if (st[iu].count (val ^ a[u])) delu = true ; } if (!delu) for (auto val:st[iv]) st[iu].insert (val); st[iv] = {}; } if (delu){ st[iu] = {}; ans++; } } int main () { int n = read (); rep (i,1 ,n+1 ) { a[i] = read (); p[i] = i; } rep (i,1 ,n){ int u = read (); int v = read (); e[u].pb (v); e[v].pb (u); } dfs (1 ,0 ,0 ); printf ("%d\n" ,ans); }
F 题目 https://codeforces.com/contest/1709/problem/F
读入 n, k ,f
包含长度等于n的字符串 的可重集合 beautiful 定义为
对于长度 [1,n] 之间任意字符串s, c[s] >= 集合中以s为前缀的字符串个数
任务 对于[1,n] 的所有字符串s, 你需要计算有多少 中映射c[s] 的选择方式
max(beautiful的集合的元素个数 )= f
其中 0 <= c[s] <= k
范围 n 15
k,f 2e5
6s
512ms
题解 我的思路 显然关于前缀
如果 s0 是 s1的前缀
那么显然 s0 出现的次数 >= s1 出现的次数
那么 如果 c[s0] <= c[s1] , c[s1] 就没什么作用
反过来, 如果 c[s0] >= sum c[s0的所有下一级后缀], 那么c[s0] 也不是紧限制
因此 真正有效的内容是
c[s1] <= c[s0] ,c[s0] <= sum c[s0的所有下一级后缀],
而这样约束以后, 方案数 就 = c[0] + c[1], 因为每个都表示能达到 的最大个数
问题来了, 这样如何 算原始的c呢
题解 转化一下题目
高度n的满二叉树
你需要对每个父子之间做最大流量限制,[0,k] 之间整数
让根源, 所有叶子汇, 的最大流恰好 = f
求方案数 % 998244353
对于给定k
a(n,f) = 高度n, 最大恰好f的方案数
考虑状态转移
那么也就是 左边的n-1高度 最大流为v的时候,右边为f-v
但是这个 左边的最大流 可能是被根到左边这个边限制的, 也可能是本身n-1,v 的方案
定义 b(n,f) 等于 高度n 且根上多一个[0,k] 的限制的最大流 = k,方案数
如果是下面子树最大f, 那么对于根上可以选择 f~k, 所以有(k-f+1)a(n,f) 种
如果是下面子树最大 > f, 那么对于根上仅可选择f, 所以有 $\sum_{i=f+1}^{2k} a(n,i) 种
综上$b(n,f) = (k-f+1)a(n,f) + \sum_{i=f+1}^{2k} a(n,i)$ , 前缀和每个状态 均摊O(1) 可算
那么对于a就是最开始的转移
$a(n,f) = \sum_{i=0}^f b(n-1,i)\cdot b(n-1,f-i)$, 卷积, 需要FFT或者NTT就可以搞
那么答案 = $a(n,f)$, 也说明 $f > 2k$ 无解
最后边界处理, $b(n,f) = 0, f > k$
关于实现, 似乎直接NTT c++17会被卡, 可能要c++20
因为这里 相当于 是$a[n] = b[n-1]^2$, 所以平方可以少算一次NTT
然后 注意计算前把 末尾0删了, 不然可能长度倍增
以及我本地测 带了-fsanitize
的编译, c++17/20 都4.5s, 而不带的都是1s
代码 https://codeforces.com/contest/1709/submission/165613575
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 #include <bits/stdc++.h> using namespace std;typedef long long ll;#define rep(i,a,n) for (ll i=a;i<(ll)n;i++) #define per(i,a,n) for (ll i=n;i-->(ll)a;) #define pb push_back ll read () {ll r;scanf ("%lld" ,&r);return r;} namespace NTT998{ const int MOD = 998244353 ; const int MAXPWR = 22 ; const int g = 3 ; const int invg = 332748118 ; int rev (int x, int len) { int ans = 0 ; while (len -- ){ ans <<= 1 ; ans |= x & 1 ; x >>= 1 ; } return ans; } inline int getlog2 (int n) { return 31 - __builtin_clz(n);} ll mypow (ll a, ll k) { ll res = 1 ; while (k) { if (k & 1 ) (res *= a) %= MOD; (a *= a) %= MOD; k >>= 1 ; } return res; } void NTT (vector<ll> &A, int flag = 1 ) { int n = A.size (); if (n == 1 ) return ; int lgn = getlog2 (n); rep (i, 0 , n) { int j = rev (i, lgn); if (j > i) swap (A[i], A[j]); } rep (pwr,0 ,lgn){ int m = 1 << pwr; ll gn = mypow (flag == 1 ? g : invg, (MOD - 1 ) / (m << 1 )); for (int k = 0 ; k < n; k += (m<<1 )) { ll gi = 1 ; rep (j,0 ,m) { auto U = A[k + j]; auto T = gi * A[k + j + m] % MOD; A[k + j] = (U + T) % MOD; A[k + j + m] = (U - T + MOD) % MOD; (gi *= gn) %= MOD; } } } if (flag == -1 ){ const ll INVSIZE = mypow (n, MOD-2 ); rep (i,0 ,n) (A[i] *= INVSIZE) %= MOD; } } void INTT (vector<ll> &A) { NTT (A,-1 );} vector<ll> poly_sq (vector<ll> &v0) { int sz = v0.size () * 2 ; if (sz == 0 )return {}; sz = 1 << (getlog2 (sz) + !!(sz & (sz-1 ))); v0.resize (sz,0 ); NTT (v0); vector<ll> a2 (sz,0 ) ; rep (i,0 ,sz) a2[i] = v0[i] * v0[i] % MOD; INTT (a2); return a2; } } int n;vector<ll> a; vector<ll> b; int main () { const int MOD = NTT998::MOD; int n = read (); int k = read (); int f = read (); if (f > 2 * k){ printf ("0\n" ); return 0 ; } b = vector <ll>(k+1 ,1 ); rep (i,1 ,n+1 ){ a = NTT998::poly_sq (b); if (a.size () <= 2 *k+1 ) a.resize (2 *k+1 ,0 ); vector<ll> prea (2 *k+2 , 0 ) ; rep (j,0 ,2 *k+1 ) prea[j+1 ] = (prea[j] + a[j]) % MOD; b = vector <ll>(k+1 ,0 ); rep (j,0 ,k+1 ) b[j] = (k-j+1 ) * a[j] % MOD + (prea[2 *k+1 ] - prea[j+1 ]); while (!b.empty () && b.back () == 0 ) b.pop_back (); } printf ("%lld\n" , a[f]); return 0 ; }
总结 E
一个是细节想错, lca多算一次 没想到
另一个是 考虑最深的lca的根 ,而不是考虑端点
这两点都是能想到没想到
然后关键的来了, 就是启发式合并的知识点, 这下学会了又
F
一个是变成树的 “流计算”, 很神奇 我好想 竖着觉得是树 ,横着就没发现了,
一个是学一下NTT, 以及NTT中的平方
参考 官方
NTT