简介
树形 DP 的主要思想是在树上对每个结点设计状态,并且父结点的状态可以从子结点转移。
树的直径
给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。
现在请你找到树中的一条最长路径。
换句话说,要找到一条路径,使得使得路径两端的点的距离最远。
注意:路径中可以只包含一个点。
题目链接:AcWing 1072 。
两次最长路(不含负权边)
任选一点 a a a ,找到距离它最远的点 u u u ,那么 u u u 一定是某条直径的端点。
证明:假设路径 P : a → u P:a\to u P : a → u 与一条直径 Q : b → c Q:b\to c Q : b → c
P ∩ Q = ∅ P\cap Q=\varnothing P ∩ Q = ∅ ,由树的连通性可以确定存在结点 x ∈ P , y ∈ Q x\in P,y\in Q x ∈ P , y ∈ Q 能够使得 a → x → y → u a\to x\to y \to u a → x → y → u ,那么由于 a → u a\to u a → u 是一条最长路,那么一定有:
w ( x → u ) ≥ w ( x → y ) + w ( y → c ) w(x\to u) \ge w(x\to y) + w(y\to c)
w ( x → u ) ≥ w ( x → y ) + w ( y → c )
由于是非负权图,因此:
w ( x → u ) + w ( x → y ) ≥ w ( y → c ) w(x\to u) + w(x\to y)\ge w(y\to c)
w ( x → u ) + w ( x → y ) ≥ w ( y → c )
两边同时加上 w ( b → y ) w(b\to y) w ( b → y ) 就能证明:
w ( b → y ) + w ( y → x ) + w ( x → u ) ≥ w ( b → y ) + w ( y → c ) w(b\to y) + w(y\to x) + w(x\to u) \ge w(b\to y) + w(y\to c)
w ( b → y ) + w ( y → x ) + w ( x → u ) ≥ w ( b → y ) + w ( y → c )
由于 Q Q Q 是一条直径,那么就会有:
w ( b → y → x → u ) ≤ w ( b → c ) w(b\to y\to x \to u)\le w(b\to c)
w ( b → y → x → u ) ≤ w ( b → c )
所以它们相等。
P ∩ Q ≠ ∅ P\cap Q\ne \varnothing P ∩ Q = ∅ ,设 x ∈ P ∩ Q x\in P\cap Q x ∈ P ∩ Q ,有:
w ( x → u ) ≥ w ( x → c ) w(x\to u) \ge w(x\to c)\\
w ( x → u ) ≥ w ( x → c )
下面步骤和情况 1 一样,同理可证。
这个求最长路用 Dijkstra 做就行,不再演示了,复杂度 O ( ( n + m ) log n ) O((n+m)\log n) O (( n + m ) log n )
树形 DP(不受边权影响)
直径一定是某个顶点的子树中的所有路径的最大路径加上次大路径(可以与前者相同),这点和前面做过的次小生成树问题不同,那里的次大值必须是严格小于最大值,否则会出现问题。
这里就用 DFS 递归去找了,其返回的是从当前结点出发的最大路径,这里证明一下为什么不需要求子树的次大路径:对于根结点 u u u 和子结点 v i v_i v i ,用 d 1 , d 2 d_1,d_2 d 1 , d 2 表示最大和次大值。
如果 d 1 , d 2 d_1,d_2 d 1 , d 2 连接在同一个结点 v i v_i v i 上,这是不符合直径的要求的。
如果 d 1 , d 2 d_1,d_2 d 1 , d 2 分别连接在 v i ≠ v j v_i\ne v_j v i = v j 上,那么根据定义一定有 d 2 ( v j ) ≤ d 1 ( v j ) d_2(v_j)\le d_1(v_j) d 2 ( v j ) ≤ d 1 ( v j ) ,从而 d 1 ( v j ) d_1(v_j) d 1 ( v j ) 就是整个的次大值,而不是 d 2 ( v j ) d_2(v_j) d 2 ( v j ) ,证毕。
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 #include <cstdio> #include <algorithm> #include <cstring> using namespace std; const int N = 10010, M = 20010; int h[N], e[M], w[M], ne[M], idx; int ans; void add(int a, int b, int c) { e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; } int dfs(int u, int fa) { // 初始化成零可以起到不考虑最大路径为负数的效果 // 因为路径中可以只包含一个点 int d1 = 0, d2 = 0; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (j == fa) continue; int d = dfs(j, u) + w[i]; // 如果等于最大值, 也要更新次大值, 说明有两条等大的最大路径 if (d >= d1) d2 = d1, d1 = d; else if (d > d2) d2 = d; } ans = max(ans, d1 + d2); return d1; } int main() { int n, m; memset(h, -1, sizeof h); scanf("%d", &n); for (int i = 0; i < n-1; i++) { int a, b, c; scanf("%d%d%d", &a, &b, &c); add(a, b, c), add(b, a, c); } dfs(1, -1); printf("%d\n", ans); return 0; }
数字转换
如果一个数 x 的约数之和 y(不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。
例如,4 可以变为 3,1 可以变为 7。
限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。
题目链接:AcWing 1075 。
本题要求 1 ~ n
中每个数的约数之和减去其自身,这里有两个方法。
分解质因子 过不了
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 typedef long long LL;const int N = 50010 ;vector<int > primes; int d[N];bool st[N];LL qmi (LL a, LL k) { LL res = 1 ; while (k) { if (k & 1 ) res *= a; a *= a; k >>= 1 ; } return res; } void init (int n) { for (int i = 2 ; i <= n; i++) { if (!st[i]) primes.push_back (i); for (int j = 0 ; primes[j] <= n/i; j++) { st[i*primes[j]] = true ; if (i % primes[j] == 0 ) break ; } } for (int i = 2 ; i <= n; i++) { int j = i; d[i] = 1 ; for (int p: primes) { if (j % p == 0 ) { int cnt = 0 ; while (j % p == 0 ) j /= p, cnt++; d[i] *= (qmi (p, cnt+1 )-1 )/(p-1 ); } } d[i] -= i; } }
分析一下复杂度,首先线筛是 O ( n ) O(n) O ( n ) ;然后对 2 ∼ n 2\sim n 2 ∼ n 枚举所有质数,有 n / ln n n/\ln n n / ln n 个;对于每个质数 p p p 都进行 log p n \log_p n log p n 次分解(快速幂是 log 2 log p n \log_2\log _p n log 2 log p n ,这步忽略不计了);综合起来大概是 O ( n 2 ) O(n^2) O ( n 2 ) 的一个复杂度,这还不如每个都试除 O ( n n ) O(n\sqrt n) O ( n n ) 的复杂度来的快。
所以大家要避免使用这个方法!!!!
枚举倍数 AC
对于 i = 1 ∼ n i=1\sim n i = 1 ∼ n 的每个数,都把它累加到 i × j i\times j i × j 上,这样的复杂度是 O ( ∑ ( n / i ) ) = O ( n log n ) O(\sum (n/i))=O(n\log n) O ( ∑ ( n / i )) = O ( n log n ) ,比试除要优很多。
由于每个数的约数之和一定是一个定值,也就是每个结点至多有一个父结点,也就说明了这是一个森林(不止一棵树)。
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 #include <cstdio> #include <cstring> #include <algorithm> using namespace std;const int N = 50010 , M = 2 *N;int h[N], e[M], ne[M], n, idx;bool st[N];int d[N], res;void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; } int dfs (int u) { if (st[u]) return -1e9 ; st[u] = true ; int d1 = 0 , d2 = 0 ; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; int d = dfs (j) + 1 ; if (d >= d1) d2 = d1, d1 = d; else if (d > d2) d2 = d; } res = max (res, d1 + d2); return d1; } int main () { memset (h, -1 , sizeof h); scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) for (int j = 2 ; j <= n/i; j++) d[i*j] += i; for (int i = 2 ; i <= n; i++) if (d[i] < i) add (d[i], i), add (i, d[i]); for (int i = 1 ; i <= n; i++) dfs (i); printf ("%d\n" , res); return 0 ; }
其实可以只加从小到大的有向边,但是本题没卡空间,因此不这么干了。
树的中心
给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。
请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。
题目链接:AcWing 1073 。
一个点的最远距离分两种情况,要么是向上走,要么向下走,记录向下走的最大路径和次大路径分别是 d 1 , d 2 d_1, d_2 d 1 , d 2
当存在相邻结点 a ↔ b ↔ c a\leftrightarrow b \leftrightarrow c a ↔ b ↔ c 时,对于 b b b :
向下走的最大路径是:
d 1 ( b ) = max ∀ c i { max { d 1 ( c i ) , 0 } + w ( b , c i ) } d_1(b)=\max_{\forall c_i}\{\max\{d_1(c_i),0\}+w(b,c_i)\}
d 1 ( b ) = ∀ c i max { max { d 1 ( c i ) , 0 } + w ( b , c i )}
当 d 1 ( c i ) d_1(c_i) d 1 ( c i ) 为负时把它舍掉,直接用 w ( b , c i ) w(b,c_i) w ( b , c i ) 作为向下走的最大路径。
向上走的最大路径是:
u p ( b ) = { max { d 1 ( a ) , u p ( a ) , 0 } + w ( a , b ) , b ∉ d 1 ( a ) max { d 2 ( a ) , u p ( a ) , 0 } + w ( a , b ) , b ∈ d 1 ( a ) up(b)=\left\{\begin{aligned}
&\max\{d_1(a),up(a),0\}+w(a,b),b\notin d_1(a)\\
&\max\{d_2(a),up(a),0\}+w(a,b),b\in d_1(a)
\end{aligned}\right.
u p ( b ) = { max { d 1 ( a ) , u p ( a ) , 0 } + w ( a , b ) , b ∈ / d 1 ( a ) max { d 2 ( a ) , u p ( a ) , 0 } + w ( a , b ) , b ∈ d 1 ( a )
当存在两条相同的最大路径时,d 1 = d 2 d_1=d_2 d 1 = d 2 ,所以这种情况会被正确处理;当 d 1 , d 2 , u p d_1,d_2,up d 1 , d 2 , u p 都为负时,就只保留 w ( a , b ) w(a,b) w ( a , b ) 作为向上走的最大路径。
接下来分析特殊结点的性质:
对于根节点,它的路径一定是向下的,因此 u p ( r o o t ) = − ∞ , d 1 ( r o o t ) ≠ − ∞ up(root)=-\infin,d_1(root)\ne -\infin u p ( roo t ) = − ∞ , d 1 ( roo t ) = − ∞ ;
对于叶子结点,它的路径一定是向上的,因此 d 1 ( v l ) = d 2 ( v l ) = − ∞ , u p ( v l ) ≠ − ∞ d_1(v_l)=d_2(v_l)=-\infin,up(v_l)\ne -\infin d 1 ( v l ) = d 2 ( v l ) = − ∞ , u p ( v l ) = − ∞
综上,一个点到其它点的最远距离就是 max { d 1 ( v ) , u p ( v ) } \max\{d_1(v),up(v)\} max { d 1 ( v ) , u p ( v )}
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 #include <cstdio> #include <algorithm> #include <cstring> using namespace std;#define max3(a, b, c) max(a, max(b, c)) const int N = 10010 , M = 20010 , INF = 0x3f3f3f3f ;int h[N], e[M], w[M], ne[M], idx;int d1[N], d2[N], g1[N], up[N];int ans = 1e9 ;void add (int a, int b, int c) { e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; } void dfs_d (int u, int fa) { for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (j == fa) continue ; dfs_d (j, u); int d = max (d1[j], 0 ) + w[i]; if (d >= d1[u]) d2[u] = d1[u], d1[u] = d, g1[u] = j; else if (d > d2[u]) d2[u] = d; } } void dfs_u (int u, int fa) { for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (j == fa) continue ; if (g1[u] == j) up[j] = max3 (up[u], d2[u], 0 ) + w[i]; else up[j] = max3 (up[u], d1[u], 0 ) + w[i]; dfs_u (j, u); } } int main () { int n; memset (up, -0x3f , sizeof up); memset (d1, -0x3f , sizeof d1); memset (d2, -0x3f , sizeof d2); memset (h, -1 , sizeof h); scanf ("%d" , &n); for (int i = 0 ; i < n-1 ; i++) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); add (a, b, c), add (b, a, c); } dfs_d (1 , -1 ); dfs_u (1 , -1 ); int res = INF; for (int i = 1 ; i <= n; i++) { res = min (res, max (d1[i], up[i])); } printf ("%d\n" , res); return 0 ; }
[HAOI2009] 毛毛虫
“毛毛虫” 的大小定义为一条路径上能通过一条边到达的所有点的数量。
输入格式
输入中第一行两个整数 N , M N, M N , M ,分别表示树中结点个数和树的边数。
接下来 M M M 行,每行两个整数 a , b a, b a , b 表示点 a a a 和点 b b b 有边连接(a , b ≤ N a, b \le N a , b ≤ N )。你可以假定没有一对相同的 ( a , b ) (a, b) ( a , b ) 会出现一次以上。
输出格式
输出一行一个整数 , 表示最大的毛毛虫的大小。
对于 100 % 100\% 100% 的数据,1 ≤ N ≤ 300000 1\leq N \le 300000 1 ≤ N ≤ 300000 。
设 f u f_u f u 是以 u u u 为根的最大值,这里设 1 1 1 为根,c n t u cnt_u c n t u 代表 u u u 的所有子结点的数量:
当 u u u 为叶结点时,f u = 1 f_u=1 f u = 1
当 u u u 有唯一儿子时,f u = f v + 1 f_u=f_v+1 f u = f v + 1
当 u u u 有多个儿子时,如果 u u u 是根节点,那么:
f u = f 1 + f 2 + c n t u − 2 + 1 = f 1 + f 2 + c n t u − 1 f_u=f_1+f_2+cnt_u-2+1=f_1+f_2+cnt_u-1
f u = f 1 + f 2 + c n t u − 2 + 1 = f 1 + f 2 + c n t u − 1
如果 u u u 不是根节点,那么:
f u = f 1 + f 2 + c n t u − 2 + 1 + 1 = f 1 + f 2 + c n t u f_u=f_1+f_2+cnt_u-2+1+1=f_1+f_2+cnt_u
f u = f 1 + f 2 + c n t u − 2 + 1 + 1 = f 1 + f 2 + c n t u
其中 f 1 , f 2 f_1,f_2 f 1 , f 2 代表子结点中的最大和非严格次大值。
可以证明,如果存在多个儿子,“分叉” 的选择一定更优:
f 1 + f 2 + c n t u − 1 > f 1 + c n t u f_1+f_2+cnt_u-1\gt f_1+cnt_u
f 1 + f 2 + c n t u − 1 > f 1 + c n t u
因为 c n t u > 1 cnt_u\gt 1 c n t u > 1 是成立的。
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 #include <cstdio> #include <cstring> #include <algorithm> using namespace std;const int N = 300010 , M = 2 *N;int n, m, ans;int h[N], f[N], e[M], ne[M], idx;void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; } int dfs (int u, int fa) { if (f[u]) return f[u]; int s1 = 0 , s2 = 0 , cnt = 0 ; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (j == fa) continue ; int s = dfs (j, u); cnt++; if (s > s1) s2 = s1, s1 = s; else if (s > s2) s2 = s; } int res = -1 ; if (cnt >= 2 ) res = s1 + s2 + cnt - !fa, f[u] = s1 + cnt; else if (cnt == 1 ) res = f[u] = s1 + 1 ; else res = f[u] = 1 ; ans = max (ans, res); return f[u]; } int main () { memset (h, -1 , sizeof h); scanf ("%d%d" , &n, &m); for (int i = 0 , a, b; i < m; i++) { scanf ("%d%d" , &a, &b); add (a, b), add (b, a); } dfs (1 , 0 ); return !printf ("%d\n" , ans); }
[CTSC1997] 选课(树形背包)
第一行有两个整数 N N N , M M M 用空格隔开。( 1 ≤ N ≤ 300 1 \leq N \leq 300 1 ≤ N ≤ 300 , 1 ≤ M ≤ 300 1 \leq M \leq 300 1 ≤ M ≤ 300 )
接下来的 N N N 行,第 I + 1 I+1 I + 1 行包含两个整数 $k_i $和 s i s_i s i , k i k_i k i 表示第I门课的直接先修课,s i s_i s i 表示第I门课的学分。若 k i = 0 k_i=0 k i = 0 表示没有直接先修课(1 ≤ k i ≤ N 1 \leq {k_i} \leq N 1 ≤ k i ≤ N , 1 ≤ s i ≤ 20 1 \leq {s_i} \leq 20 1 ≤ s i ≤ 20 )。
输出选 M M M 门课程的最大得分。
题目链接:P2014 。
树形背包有一个 O ( n m 2 ) O(nm^2) O ( n m 2 ) 的做法,但通过 DFS 序可以优化到 O ( n m ) O(nm) O ( nm ) ,这里就不提那个更劣的解法了。
DFS 序有一个很好的性质,就是对于结点 u u u ,与它相邻的不包含在它子树内的编号是 d f n ( u ) + s i z e ( u ) dfn(u)+size(u) df n ( u ) + s i ze ( u ) 。
我们设状态 f ( i , j ) f(i,j) f ( i , j ) 是 DFS 在 i ∼ n i\sim n i ∼ n 内,体积为 j j j 的选择,那么对于每个结点都分为两种情况:
选子树,状态是 f ( i + 1 , j − v ( u ) ) + w ( u ) f(i+1,j-v(u))+w(u) f ( i + 1 , j − v ( u )) + w ( u )
不选子树,状态是 f ( i + s i z e ( u ) , j ) f(i+size(u),j) f ( i + s i ze ( u ) , j )
如果是叶节点,这样操作的结果就是用比它 DFS 序靠后的叶结点来更新它的最大价值:
f ( i , j ) = max { f ( i + 1 , j − v ( u ) ) + w ( u ) f ( i + 1 , j ) f(i,j)=\max\begin{cases}
f(i+1,j-v(u))+w(u)\\
f(i+1,j)
\end{cases}
f ( i , j ) = max { f ( i + 1 , j − v ( u )) + w ( u ) f ( i + 1 , j )
因此我们在这个叶子结点的父结点时就能正确取到 i ∼ n i\sim n i ∼ n 的最大价值。因此,我们需要倒序遍历子结点,这样能保证状态该转移的时候已经被计算出来,所以这里选择用 vector
存图比较方便。
注意,由于我们处理的是一个森林,所以虚拟出来一个超级源点 0 0 0 ,它的体积同样也是 1 1 1 ,因此要在程序开头给 m m m 加一。
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 #include <cstdio. #include <vector> #include <algorithm> using namespace std;const int N = 310 ;int n, m;int f[N][N], w[N], sz[N], dfn[N], cnt;vector<int > g[N]; int dfs (int u) { dfn[u] = ++cnt, sz[u] = 1 ; for (int v: g[u]) sz[u] += dfs (v); return sz[u]; } void dp (int u) { for (int i = g[u].size () - 1 ; i >= 0 ; i--) dp (g[u][i]); for (int j = 1 ; j <= m; j++) f[dfn[u]][j] = max (f[dfn[u]+1 ][j-1 ]+w[u], f[dfn[u]+sz[u]][j]); } int main () { scanf ("%d%d" , &n, &m), ++m; for (int i = 1 , fa; i <= n; i++) { scanf ("%d%d" , &fa, &w[i]); g[fa].emplace_back (i); } dfs (0 ), dp (0 ); return !printf ("%d\n" , f[1 ][m]); }
[SDOI2006] 保安站岗(最小点覆盖)
给定一棵 N ( 1 ≤ N ≤ 1500 ) N(1\le N\le 1500) N ( 1 ≤ N ≤ 1500 ) 个点的树,每条边都必须被覆盖(即每条边的两个端点至少选择一个),求满足条件的最小点权和。
题目链接:P2458 。
考虑每个点都有三种情况:在这个点放置、在这个点的父结点放置、这个点的子结点放置。分别用 0 / 1 / 2 0/1/2 0/1/2 表示这些状态。
f ( u , 0 ) = w u + ∑ j ∈ s o n u min { f ( j , 0 ) , f ( j , 1 ) , f ( j , 2 ) } f ( u , 1 ) = ∑ j ∈ s o n u min { f ( j , 0 ) , f ( j , 2 ) } f ( u , 2 ) = ∑ j ∈ s o n u min { f ( j , 0 ) , f ( j , 2 ) } , ∃ f ( j , 0 ) \begin{aligned}
f(u,0)&=w_u+\sum_{j\in son_u} \min\{f(j,0),f(j,1),f(j,2)\}\\
f(u,1)&=\sum_{j\in son_u} \min\{f(j,0),f(j,2)\}\\
f(u,2)&=\sum_{j\in son_u} \min\{f(j,0),f(j,2)\},\exist f(j,0)\\
\end{aligned}
f ( u , 0 ) f ( u , 1 ) f ( u , 2 ) = w u + j ∈ so n u ∑ min { f ( j , 0 ) , f ( j , 1 ) , f ( j , 2 )} = j ∈ so n u ∑ min { f ( j , 0 ) , f ( j , 2 )} = j ∈ so n u ∑ min { f ( j , 0 ) , f ( j , 2 )} , ∃ f ( j , 0 )
对于 f ( u , 2 ) f(u,2) f ( u , 2 ) 我们必须选出一个 j j j 使它的状态是在这个点放置,这样才能保证 u u u 被观察到。
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 #include <cstdio> #include <cstring> #include <algorithm> using namespace std;const int N = 1550 , M = 3010 , INF = 0x3f3f3f3f ;int h[N], w[N], e[M], ne[M], idx;int n, f[N][3 ];void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; } void dfs (int u, int fa) { f[u][0 ] = w[u], f[u][1 ] = 0 , f[u][2 ] = INF; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (j == fa) continue ; dfs (j, u); f[u][0 ] += min (min (f[j][0 ], f[j][1 ]), f[j][2 ]); f[u][1 ] += min (f[j][0 ], f[j][2 ]); } for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (j == fa) continue ; f[u][2 ] = min (f[u][2 ], f[u][1 ] - min (f[j][0 ], f[j][2 ]) + f[j][0 ]); } } int main () { memset (h, -1 , sizeof h); scanf ("%d" , &n); for (int i = 1 , id, m; i <= n; i++) { scanf ("%d" , &id), scanf ("%d%d" , &w[id], &m); for (int j = 0 , s; j < m; j++) scanf ("%d" , &s), add (id, s), add (s, id); } dfs (1 , 0 ); return !printf ("%d\n" , min (f[1 ][0 ], f[1 ][2 ])); }
[CSPS2019] 括号树
题目背景
本题中合法括号串 的定义如下:
()
是合法括号串。
如果 A
是合法括号串,则 (A)
是合法括号串。
如果 A
,B
是合法括号串,则 AB
是合法括号串。
本题中子串 与不同的子串 的定义如下:
字符串 S
的子串是 S
中连续 的任意个字符组成的字符串。S
的子串可用起始位置 l l l 与终止位置 r r r 来表示,记为 S ( l , r ) S (l, r) S ( l , r ) (1 ≤ l ≤ r ≤ ∣ S ∣ 1 \leq l \leq r \leq |S | 1 ≤ l ≤ r ≤ ∣ S ∣ ,∣ S ∣ |S | ∣ S ∣ 表示 S 的长度)。
S
的两个子串视作不同当且仅当 它们在 S
中的位置不同,即 l l l 不同或 r r r 不同。
题目描述
一个大小为 n n n 的树包含 n n n 个结点和 n − 1 n - 1 n − 1 条边,每条边连接两个结点,且任意两个结点间有且仅有 一条简单路径互相可达。
小 Q 是一个充满好奇心的小朋友,有一天他在上学的路上碰见了一个大小为 n n n 的树,树上结点从 1 ∼ n 1 \sim n 1 ∼ n 编号,1 1 1 号结点为树的根。除 1 1 1 号结点外,每个结点有一个父亲结点,u u u (2 ≤ u ≤ n 2 \leq u \leq n 2 ≤ u ≤ n )号结点的父亲为 f u f_u f u (1 ≤ f u < u 1 ≤ f_u < u 1 ≤ f u < u )号结点。
小 Q 发现这个树的每个结点上恰有 一个括号,可能是(
或)
。小 Q 定义 s i s_i s i 为:将根结点到 i i i 号结点的简单路径上的括号,按结点经过顺序依次排列组成的字符串。
显然 s i s_i s i 是个括号串,但不一定是合法括号串,因此现在小 Q 想对所有的 i i i (1 ≤ i ≤ n 1\leq i\leq n 1 ≤ i ≤ n )求出,s i s_i s i 中有多少个互不相同的子串 是合法括号串 。
这个问题难倒了小 Q,他只好向你求助。设 s i s_i s i 共有 k i k_i k i 个不同子串是合法括号串, 你只需要告诉小 Q 所有 i × k i i \times k_i i × k i 的异或和,即:
( 1 × k 1 ) xor ( 2 × k 2 ) xor ( 3 × k 3 ) xor ⋯ xor ( n × k n ) (1 \times k_1)\ \text{xor}\ (2 \times k_2)\ \text{xor}\ (3 \times k_3)\ \text{xor}\ \cdots\ \text{xor}\ (n \times k_n)
( 1 × k 1 ) xor ( 2 × k 2 ) xor ( 3 × k 3 ) xor ⋯ xor ( n × k n )
其中 x o r xor x or 是位异或运算。
输入格式
第一行一个整数 n n n ,表示树的大小。
第二行一个长为 n n n 的由(
与)
组成的括号串,第 i i i 个括号表示 i i i 号结点上的括号。
第三行包含 n − 1 n − 1 n − 1 个整数,第 i i i (1 ≤ i < n 1 \leq i \lt n 1 ≤ i < n )个整数表示 i + 1 i + 1 i + 1 号结点的父亲编号 f i + 1 f_{i+1} f i + 1 。
输出格式
仅一行一个整数表示答案。
首先考虑序列,如果一个序列 a a a 必须以 a i a_i a i 结尾的所有子括号串数量为 t i t_i t i ,在前 i i i 个字符中的所有数量为 k i k_i k i ,显然需要使 a[i]=')'
;然后 a j a_j a j 为和 a i a_i a i 匹配的括号串,需要 a[j]='('
。
如果选定了 a i a_i a i ,可以看出 a j ∼ a i a_j\sim a_i a j ∼ a i 这一段是必选的,否则不是合法括号串。而 a 1 ∼ a j − 1 a_1\sim a_{j-1} a 1 ∼ a j − 1 可选可不选,所以需要把不选的情况加上,那么可以得到转移方程:
t i = t j − 1 + 1 t_i=t_{j-1}+1
t i = t j − 1 + 1
在前 i i i 个字符中满足条件的所有子串数量累项求和即可,即对于任意 k i = k i − 1 + t i k_i=k_{i-1}+t_i k i = k i − 1 + t i
现在放到树上,也只是把 j − 1 j-1 j − 1 变成了 j j j 的父结点,因为在序列中 − 1 -1 − 1 代表上一项,树中父结点就是上一项。
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 #include <cstdio> #include <stack> #include <vector> using namespace std;typedef long long LL;const int N = 500010 ;int n, fa[N];LL t[N], k[N]; char s[N];stack<int > stk; vector<int > g[N]; void dfs (int u) { k[u] = k[fa[u]]; if (s[u] == '(' ) { stk.push (u); for (int j: g[u]) dfs (j); return stk.pop (); } if (stk.size ()) { int p = stk.top (); stk.pop (); t[u] = t[fa[p]] + 1 ; k[u] += t[u]; for (int j: g[u]) dfs (j); return stk.push (p); } for (int j: g[u]) dfs (j); } int main () { scanf ("%d%s" , &n, s+1 ); for (int i = 2 ; i <= n; i++) scanf ("%d" , &fa[i]), g[fa[i]].emplace_back (i); dfs (1 ); LL res = 0 ; for (int i = 1 ; i <= n; i++) res ^= i*k[i]; return !printf ("%lld\n" , res); }
[USACO12FEB] Nearby Cows G
给定一棵 n n n 个点的树,点带权,对于每个节点求出距离它不超过 k k k 的所有节点权值和 m m m 。
题目链接:P3047 。
思路是预处理出 f ( u , k ) f(u,k) f ( u , k ) 表示结点 u u u 向下走 k k k 层的所有点权之和,很容易得出递推式:
f ( u , k ) = w u + ∑ j ∈ s o n u f ( j , k − 1 ) f(u,k)=w_u+\sum_{j\in son_u} f(j,k-1)
f ( u , k ) = w u + j ∈ so n u ∑ f ( j , k − 1 )
查询的时候,我们就一级一级的祖先查,写一个循环即可。注意这里要用到一个容斥,设 p r e pre p re 是上一次跳转的结点,那么当前加上的权重应当是:
f ( u , i ) − f ( p r e , i − 1 ) f(u,i)-f(pre,i-1)
f ( u , i ) − f ( p re , i − 1 )
这样避免重复计算。
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 #include <cstdio> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 , M = 200010 , K = 25 ;int h[N], w[N], e[M], ne[M], idx;int fa[N], f[N][K];int n, k;void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; } void dfs (int u, int father) { fa[u] = father; for (int i = 0 ; i <= k; i++) f[u][i] = w[u]; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (j == father) continue ; dfs (j, u); for (int p = 1 ; p <= k; p++) f[u][p] += f[j][p-1 ]; } } int get (int u) { int res = 0 , i = k, pre = 0 ; while (i >= 0 && u) { res += f[u][i]; if (pre != 0 ) res -= f[pre][i-1 ]; pre = u, u = fa[u], i--; } return res; } int main () { memset (h, -1 , sizeof h); scanf ("%d%d" , &n, &k); for (int i = 1 , u, v; i < n; i++) scanf ("%d%d" , &u, &v), add (u, v), add (v, u); for (int i = 1 ; i <= n; i++) scanf ("%d" , &w[i]); dfs (1 , 0 ); for (int i = 1 ; i <= n; i++) printf ("%d\n" , get (i)); return 0 ; }
[USACO17DEC] Barn Painting G(计数 DP)
给定一颗 N N N 个节点组成的树,你要给每个点涂上三种颜色之一,其中有 K K K 个节点已染色,要求任意两相邻节点颜色不同,求合法染色方案数。答案对 1 0 9 + 7 10^9+7 1 0 9 + 7 取模。
题目链接:P4084 。
可以设 f ( u , c ) f(u,c) f ( u , c ) 表示当前点涂 c c c 颜色的方案数,如果输入已经给定了 u u u 结点的颜色,就把其它颜色设为 0 0 0 ,否则都设为 1 1 1 。
容易得到转移方程:
f ( u , 1 ) = ∏ j ∈ s o n u [ f ( j , 2 ) + f ( j , 3 ) ] f ( u , 2 ) = ∏ j ∈ s o n u [ f ( j , 1 ) + f ( j , 3 ) ] f ( u , 3 ) = ∏ j ∈ s o n u [ f ( j , 1 ) + f ( j , 2 ) ] f(u,1)=\prod_{j\in son_u} [f(j,2)+f(j,3)]\\
f(u,2)=\prod_{j\in son_u} [f(j,1)+f(j,3)]\\
f(u,3)=\prod_{j\in son_u} [f(j,1)+f(j,2)]
f ( u , 1 ) = j ∈ so n u ∏ [ f ( j , 2 ) + f ( j , 3 )] f ( u , 2 ) = j ∈ so n u ∏ [ f ( j , 1 ) + f ( j , 3 )] f ( u , 3 ) = j ∈ so n u ∏ [ f ( j , 1 ) + f ( j , 2 )]
这里一开始用 0 , 1 , 2 0,1,2 0 , 1 , 2 当颜色坑了我半天,因为我直接用 i f ( c o l o r ( u ) ) if(color(u)) i f ( co l or ( u )) 判断,而 0 0 0 又是一个颜色,所以就疯狂 WA,还是不省这点空间了。
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 #include <cstdio> #include <cstring> #include <algorithm> using namespace std;typedef long long LL;const int N = 100010 , M = 200010 , P = 1e9 +7 ;int h[N], color[N], e[M], ne[M], idx;int n, k;LL f[N][4 ]; void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; } void dfs (int u, int fa) { if (color[u]) f[u][color[u]] = 1 ; else f[u][1 ] = f[u][2 ] = f[u][3 ] = 1 ; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (j == fa) continue ; dfs (j, u); f[u][1 ] = f[u][1 ] * (f[j][2 ] + f[j][3 ]) % P; f[u][2 ] = f[u][2 ] * (f[j][1 ] + f[j][3 ]) % P; f[u][3 ] = f[u][3 ] * (f[j][1 ] + f[j][2 ]) % P; } } int main () { memset (h, -1 , sizeof h); scanf ("%d%d" , &n, &k); for (int i = 1 , a, b; i < n; i++) { scanf ("%d%d" , &a, &b); add (a, b), add (b, a); } for (int i = 0 , a, b; i < k; i++) { scanf ("%d%d" , &a, &b); color[a] = b; } dfs (1 , 0 ); return !printf ("%lld\n" , (f[1 ][1 ] + f[1 ][2 ] + f[1 ][3 ]) % P); }