C https://codeforces.com/contest/1707/problem/C
给你个n点,m边的连通图,边两两不等
有个错误的dfs算法, 每次找未选择的点中最短边进行dfs
问,从哪些点开始dfs,能得到正确的最小生成树
范围 n 1e5
m [n-1,2e5]
2s
256MB
题解 我的思路 首先边两两不等,说明正确的最小生成树唯一
第二以一个点作为根, 按正确的最小生成树建树, 那么树边以外的其它边都是回边,没有跨边,则这个点做dfs合法
但这样每次枚举就是 O(n^2), TLE
题解 先找到最小生成树
如果有连接 u v的非树边
那么 通过树边的简单路径 u—–v 通过树边的中间的点, 不可能, 且沿着树边扩展的点也不可能
变成了树上染色问题
然后剩下就LCA,树上差分了
LCA + 树上差分 思想就是
初始是对不可能的+1
然后因为批量+1 复杂度大
变成了记录每个数和它父节点的差(根节点表示自身的值), 就是树上差分了
那么对于 u,v 是非祖先关系, c[根]+=1,c[u]-=1,c[v]-=1
u和v是祖先关系, 假设u是v的祖先
c[u向v的子节点]+=1, c[v]-=1
最后还原差分成真实树即可, 判断 > 0?
代码 https://codeforces.com/contest/1707/submission/164711832
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 #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 const int POWER = 20 ;const int N = 100000 ;const int M = 200000 ;ll read () {ll r;scanf ("%lld" ,&r);return r;} int n;int m;vector<int >p2[N+10 ]; pair<int ,int > e[M+10 ]; bool treee[M+10 ]; int f[N+10 ]; int find (int v) {return f[v]==v?v:(f[v] = find (f[v]));}void link (int u,int v) { f[find (v)] = find (u);}ll x[N+10 ]; int fa[N+10 ][POWER]; int d[N+10 ]; void build (int u,int p = 1 ,int dep = 1 ) { fa[u][0 ] = p; d[u] = dep; for (auto v:p2[u]) if (v != p) build (v,u,dep+1 ); } void dfs (int u, int p = 1 , int s = 0 ) { x[u] += s; for (auto v:p2[u]) if (v != p) dfs (v,u,x[u]); } int lca (int u,int v) { if (d[u] < d[v])swap (u,v); per (i,0 ,POWER) if (d[u] - d[v] >= (1 << i)) u = fa[u][i]; if (u == v) return u; per (i,0 ,POWER) { if (fa[u][i] != fa[v][i]){ u = fa[u][i]; v = fa[v][i]; } } return fa[u][0 ]; } int fa_d (int u,int d) { per (i,0 ,POWER) if (d >= (1 << i)) { d -= (1 <<i); u = fa[u][i]; } return u; } int main () { n = read (); m = read (); rep (i,0 ,m){ int u = read (); int v = read (); e[i] = {u,v}; } iota (f+1 ,f+n+1 ,1 ); rep (i,0 ,m){ auto [u,v] = e[i]; if (find (u) != find (v)) { treee[i] = true ; link (u,v); p2[u].pb (v); p2[v].pb (u); } } build (1 ); rep (pwr,1 ,POWER) rep (i,1 ,n+1 ) fa[i][pwr] = fa[fa[i][pwr-1 ]][pwr-1 ]; rep (i,0 ,m) if (!treee[i]){ auto [u,v] = e[i]; if (d[u] > d[v]) swap (u,v); int r = lca (u,v); if (u == r){ int w = fa_d (v, d[v] - d[u] - 1 ); x[w]++; x[v]--; } else { x[1 ]++; x[u]--; x[v]--; } } dfs (1 ); rep (i,1 ,n+1 ) printf ("%d" , (int )(x[i] == 0 )); return 0 ; }
D https://codeforces.com/contest/1707/problem/D
题目 n 点 根为1树
初始 U = 1..n 点集合
一次操作, 取点集T, T 是U的部分虚树(T是U的真子集, 且T中任意两点的LCA也属于T), 令U=T
求 恰好k次操作后 集合只有根节点的操作路径有多少种
方案数 mod p
要求 k=1,2,…,n-1 所有的结果
范围 n 2000
p [1e8+7 ~ 1e9+9] 是 质数
2s
256mb
题解 我的思路 可能可以倒过来
初始只有根,每次增加至少一个点, 增加后要满足LCA的在集合中的关系, 一共k次
考虑一个叶子节点, 它可以任意时候被加入
一个非叶子只有一个分支的节点, 它也是任意时刻被加入
一个非叶子,多分支节点那么它加入的时机 需要 早于或等于 它的第二个被加入的子树
换句话说, 假设点i为多叉点,在t时刻被加入,那么 1..t-1 中至多只能存在点i 的其中一个子树中的点
f(t..k, all子树)
f(1..k, 子树1, 至少一个在[1..t-1]) * f(t..k, all子树-子树1) +
f(1..k, 子树2, 至少一个在[1..t-1]) * f(t..k, all子树-子树2) +
…
但似乎 注意到一个子树放在区间至于区间长度有关, 这样至少后面一半状态上是nk的
前面一节, 可以变成f(1..k,子树1) - f(t..k, 子树1) 这样剩下的至少一个不属于(t..k)
f(根u, 长度l) = for t = 1..l
转移还要n, 这样n^3, 而且还有k, 一眼TLE
考虑到本来状态就是 f(根u, 长度l) 也就是对于不同k可以复用, 那么问题来到了如何把转移优化到 log或者常数级别, 或者均摊常数级别
f(u,l) = f(v0, k - t + 1) * f(v1)
如果干掉这个t就好了
题解 如果每次点数不严格下降结果为f,原答案为ans
有 $f_i=\sum_{j=0}^i\binom{i}{j}ans_j$, 因为j步意味着j+1个不同的集合, 要用这j+1个不同的集合按原顺序,可重复的产生i+1个集合, 那么其实就是选择每个开始的位置
既然是带系数前缀和,那也可以从 f反向推ans, 所以问题怎么球不严格下降的结果
然后又是来到和我讨论类似的对删除时间的讨论
当一个节点u有分叉时
那么它的删除时间就会受到 子树的限制
子树里所有节点 早于等于 u
u的某个子树以外的子树都删完了 早于等于 u, 早于剩下的子树最后一个节点
状态 dp[u][t]
, u以及它的子树,恰好第i次操作后删除完的方案数
转移 $dp_{u,t}$
$C_u$ 是 $u$ 的子节点集合
前缀 $S_{u,t} = \sum_{i\le t} dp_{u,i}$
$u$子集前缀关于$t$的乘积 $M_{u,t} = \prod_{v \in C_u} S_{v,t}$
$u$在$t$时刻删, 则 剩下的都在$[1,t]$时刻删
$M_{u,t}$
$u$在$t_0 < t$ 时刻删, 因为要恰好, 则至少有个在$t$时刻删, 其它在$[1,t_0]$时刻删
$ \sum_{v\in C_u} (dp_{v,t} \cdot \prod_{w \in C_u, w \neq v}^N S_{w,t_0})$
$ = \sum_{v\in C_u} (dp_{v,t} \cdot \frac{\prod_{w\in C_u} S_{w,t_0}}{S_{v,t_0}})$
$ = \sum_{v\in C_u} dp_{v,t} \cdot \frac{M_{u,t_0}}{S_{v,t_0}}$
对于所有$t_0 \in [1..t-1]$ 加和
$ = \sum_{t_0 = 1}^{t-1} (\sum_{v\in C_u} dp_{v,t} \cdot \frac{M_{u,t_0}}{S_{v,t_0}})$
$ = \sum_{v\in C_u} (dp_{v,t} \cdot (\sum_{t_0 = 1}^{t-1} \cdot \frac{M_{u,t_0}}{S_{v,t_0}}))$
综上
$ dp_{u,t} = M_{u,t} + \sum_{v\in C_u} (dp_{v,t} \cdot (\sum_{t_0 = 1}^{t-1} \cdot \frac{M_{u,t_0}}{S_{v,t_0}}))$
每个$S_{u,t}$, 均摊只需要O(1), $S$总状态$O(n^2)$, 所以时间复杂度$O(n^2)$
然后每个$M_{u,t}$ 均摊需要$O(|C_u|)$, 对于所有$u$的$|C_u|$和为 节点数,所以时间复杂度也是$O(n^2)$
再看 $\sum_{t_0 = 1}^{t-1} \cdot \frac{M_{u,t_0}}{S_{v,t_0}}$
注意和$u,v,t$ 有关,但v只能是u的子节点集, 所以状态数为$O(|C_u|n)$, 总状态数依然是$O(n^2)$, 同样通过t的前缀和, 均摊$O(1)$, 所以总时间复杂度也是$O(n^2k)$
最后$dp_{u,t}$, 状态显然$O(n^2)$, 时间复杂度$O(|C_u|)$, 所以总时间复杂度$O(n^2)$;
可做……….
咦,看起来和我的很像啊, 是不是我的那个t也可以干掉
可能不一定,别人通过恰好来简化了转移方便了前缀和实现
最后的最后, 通过$ans_i = f_i - \sum_{j=0}^{i-1} \binom{i}{j} ans_j $反推即可
实际写起来有几点坑,
时间卡得紧, 不要频繁的用费马小定理 计算inv, TLE11
Wa31 第31个点 会出现S是MOD的倍数…..
然后Wa31需要小学数学, 因为本身是乘法, 变成除法只是为了简化运算, 所以本身不会有除0, 但 变化后 加上mod 就可能除以0
注意到这里其实就是M和S之间,所以统计一下M中少乘一个零的结果, 当S=0时取那个结果即可
然后 因为量级很大, 不能去 大量做mod除法, 所以一个办法是记录 分子分母, 另一个办法是
M/S也变成前缀+后缀的形式来算, 当然前缀+后缀形式会常数更小
代码 分数+除法 857ms 158MB
https://codeforces.com/contest/1707/submission/164894672
前后缀 + longlong 733ms 81MB
https://codeforces.com/contest/1707/submission/164900140
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 #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 int MOD = -1 ;ll read () {ll r;scanf ("%lld" ,&r);return r;} ll mpow (ll v,ll mi) {ll r = 1 ;while (mi){if (mi%2 )(r*=v)%=MOD;(v*=v)%=MOD;mi>>=1 ;};return r;} int n;vector<int > e[2010 ]; void dfs (int u,int fa) { vector<int > arr = {}; for (auto v: e[u]) if (v != fa) arr.pb (v); e[u] = arr; for (auto v: e[u]) dfs (v, u); } struct ModInt { int v; ModInt (ll val = 0 ) :v (val) { } }; ll real (const ModInt & v0) { return (v0.v + MOD) % MOD; } ModInt operator +(const ModInt & v0, const ModInt &v1){ return (v0.v + v1.v) % MOD; } ModInt operator -(const ModInt & v0, const ModInt &v1){ return (v0.v - v1.v) % MOD; } ModInt operator *(const ModInt & v0, const ModInt &v1){ return ((ll)v0.v * (ll)v1.v) % MOD; } ModInt dp[2010 ][2010 ]; vector<ModInt> preMu[2010 ]; vector<ModInt> sufMu[2010 ]; ModInt S[2010 ][2010 ]; ModInt W[2010 ][2010 ]; ModInt fac[2010 ] = {1 }; ModInt invv[2010 ] = {1 }; ModInt invfac[2010 ] = {1 }; ModInt C (int n,int m) { return fac[n] * invfac[m] * invfac[n-m]; }ModInt ans[2010 ]; int main () { n = read (); MOD = read (); rep (i,1 ,n){ int u = read (); int v = read (); e[u].pb (v); e[v].pb (u); } rep (i,1 ,n+1 ) fac[i] = fac[i-1 ] * i; invv[1 ] = 1 ; rep (i,2 ,n+1 ) invv[i] = (MOD-MOD/i) * invv[MOD % i]; rep (i,1 ,n+1 ) invfac[i] = invfac[i-1 ] * invv[i]; dfs (1 , 1 ); vector<int > vorder = {1 }; rep (i, 0 , vorder.size ()) { int u = vorder[i]; for (auto v: e[u]) vorder.pb (v); } reverse (vorder.begin (), vorder.end ()); for (auto u: vorder) { rep (t,1 ,n) { ModInt &dput = dp[u][t] = 1 ; if (!e[u].empty ()) { preMu[t] = vector <ModInt> (e[u].size () + 1 , 1 ); sufMu[t] = vector <ModInt> (e[u].size () + 1 , 1 ); rep (i,0 ,e[u].size ()){ auto v = e[u][i]; preMu[t][i+1 ] = preMu[t][i] * S[v][t]; } per (i,0 ,e[u].size ()){ auto v = e[u][i]; sufMu[t][i] = sufMu[t][i+1 ] * S[v][t]; } dput = preMu[t][e[u].size ()]; if (t > 1 ) rep (i,0 ,e[u].size ()) { auto v = e[u][i]; ModInt &Wvt = W[v][t-1 ] = ((t-1 > 1 ) ? W[v][t-2 ] : 0 ) + (preMu[t-1 ][i] * sufMu[t-1 ][i+1 ]); dput = dput + dp[v][t] * Wvt; } } S[u][t] = ((t > 1 ) ? S[u][t-1 ] : 0 ) + dput; } } rep (t,1 ,n) ans[t] = preMu[t][e[1 ].size ()]; rep (t,1 ,n) rep (t0,1 ,t) ans[t] = ans[t] - ans[t0] * C (t,t0) ; rep (t,1 ,n) printf ("%lld " , real (ans[t])); return 0 ; }
E https://codeforces.com/contest/1707/problem/E
题目 长度n数组 a
$ai \in [1,n]$
f((l,r)) = (min(a[l..r]) , max(a[l..r])), 传入区间范围, 返回最小值和最大值
每次调用 (l,r) = f((l,r))
q个询问
每次问如果初始 li,ri, 需要反复调用 多少次 让l和r 最终变成(1,n) 或不可能
范围 q 1e5
n 1e5
题解 我的思路 显然对于给定l,r 输出的f是一定的
所以对于所有的输入, 全部成环
那么f( (1,n) ) 一定要等于 (1,n), 否则 只有直接传入1,n 才会满足
想倒着找, 但是显然有最坏情况, 2 3 4 5 6, 这样一共有$O(n^2)$种不同输入和结果
题解 如果 区间A 包含于区间B
那么 f(A) 包含于 f(B)
证明, min(B) <= min(A) <= max(A) <= max(B)
并且高阶f也有这个包含关系
因此如果 [l,r] = ([l,r0] 并 [l0,r]), 其中 (l0 <= r0)
那么 f(l,r) 包含 (f(l,r0) 并 f(l0,r)),
注意到 f(l,r0) 包含 f(l0,r0), f(l0,r) 也包含 f(l0,r0)
所以 f(l,r0) 和 f(l0,r) 本身就重叠, 所以 f(l,r0) 并 f(l0,r) = 连续的区间
那么 f(l,r) 的最小值 至少包含于 f(l,r0) 和 f(l0,r) 的其中一个, 最大值也是, 所以最小值最大值都存在,且连续, 包含于 f(l,r)
综上 f(l,r0) 并 f(l0,r) = f(l,r)
同样高阶也满足
例如2阶段, f(f(l,r)) = f(f(l,r0)) 并 f(f(l0,r)) , 思路同上, 包含于关系, 最小 最大值, 连续 推出相等
注意到 一旦能到整个长度,那么一定 f(1,n) = (1,n)
如果链很长, 根据状态数 可能达到n^2
那么 办法就是倍增, 倍增到> n^2 如果还不行那就真不行了
可以的话就二分倍增的倍数
为了效率, 用倍增记录每个位置开始的长度的 多少轮跳跃的结果
然后实现上几点注意, 别二分, 每次二分会导致 长度不是幂次 依然需要log, 多层log过不了
找结果依然是倍增的找
然后就是cpu缓存和tlb机制, 注意循环顺序访问顺序和数组定义顺序
这导致常数影响非常明显, tle的代码和600+ms过的代码 就是把顺序换了换
然后我看有人 长度开的2^18 也过了!!!, 不知道数学上是否有办法证明或者找反例
代码 https://codeforces.com/contest/1707/submission/164951002
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 #include <bits/stdc++.h> using namespace std; typedef long long ll;typedef pair<int ,int > pii;#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;} const int N = 100000 ;const int PLEN = 19 ; const int POP = 35 ; ll n; int L[POP][PLEN][N+10 ]; int R[POP][PLEN][N+10 ]; int LG2[N+10 ]; int a[N+10 ]; inline pii f (int l,int r,int p1) { if (l == r) return { L[p1][0 ][l], R[p1][0 ][r] }; int sz = LG2[r-l]; return { min (L[p1][sz+1 ][l],L[p1][sz+1 ][r-(1 <<sz)]), max (R[p1][sz+1 ][l],R[p1][sz+1 ][r-(1 <<sz)]) }; } inline int query (int l,int r) { if (l == 1 && r == n) return 0 ; if (l == r) return -1 ; ll ret = 0 ; per (i, 0 , POP){ int l0, r0; tie (l0, r0) = f (l, r, i); if (l0 != 1 || r0 != n){ l = l0; r = r0; ret += (1ll << i); if (l == r) return -1 ; } } tie (l,r) = f (l, r, 0 ); return (l == 1 && r == n) ? (ret + 1 ) : -1 ; } int main () { n = read (); rep (i,2 ,N) LG2[i] = LG2[i/2 ] + 1 ; int q = read (); rep (i,1 ,n+1 ) L[0 ][0 ][i] = R[0 ][0 ][i] = a[i] = read (); { const int p1 = 0 ; rep (p0,1 ,PLEN) rep (i,1 ,n+1 ) { L[p1][p0][i] = min (L[p1][p0-1 ][i],L[p1][p0-1 ][min (n,i+(1ll << max (0ll , p0-2 )))]); R[p1][p0][i] = max (R[p1][p0-1 ][i],R[p1][p0-1 ][min (n,i+(1ll << max (0ll , p0-2 )))]); } } rep (p1, 1 , POP) rep (p0, 0 , PLEN) rep (i,1 ,n+1 ){ tie ( L[p1][p0][i] , R[p1][p0][i] ) = f ( L[p1 - 1 ][p0][i], R[p1 - 1 ][p0][i], p1 - 1 ); } while (q--){ int l = read (); int r = read (); printf ("%d\n" , query (l,r)); } return 0 ; }
总结 C
有想到, 环的最大边的 两点以外的点不可能,但是有反例, 就是没有把最小生成树 和 这个合在一起考虑
后面LCA和树上差分倒是没啥问题掌握了
D
感觉真有可能能做出来, 多习惯在dp时 分别恰好,和 小于等于 ,以及 前缀和方程与逆方程之间的联系
被小学数学教育了$a \neq \frac{a\cdot b}{b}$
然后就是 大量的inv还是不要, 一个办法就是 分数表示, 一个办法是想办法消除掉除法
E
数学推出性质以后 就是倍增倍增倍增了
实现上也有一些坑
参考 官方