题目描述
给出一棵n个点的树,每个点有黑白两种颜色。q次询问,每次询问给出x和y,问能否选出一个x个点的联通子图,使得其中黑点数目为y。
输入
第一行一个正整数 T 表示数据组数。
对于每一组数据,第一行有两个用空格隔开的正整数,分别是 n 和 q ,表示树的节点数和询问次数。
接下来 n-1 行,每行两个用空格隔开的正整数和,表示和间有一条边相连。
接下来一行有 n 个用空格隔开的整数,其中若,则表示第 i 个点为白色,否则为黑色。
接下来 q 行,每行两个用空格隔开的整数 x 和 y 。
n<=5000,q<=10^5
输出
对于每一组数据,输出 q 行,每行为 “YES” 或者 “NO” (不含双引号),表示对于给定的和,能否满足小A 的要求。
每相邻两组数据的输出之间空一行。
样例输入
1
9 4
4 1
1 5
1 2
3 2
3 6
6 7
6 8
9 6
0 1 0 1 0 0 1 0 1
3 2
7 3
4 0
9 5
输出
YES
YES
NO
NO
题解
树形背包dp
(以下为考试时的想法)
n有5000之大,直接预处理能否选出包含某数目的黑点的某大小的联通子图岂不是直接GG?
于是大胆猜结论:~!@#¥%……&*()
然后写了发树形背包,交上去A了。。
(以上为考试时的想法)
结论:.对于某一大小的连通子图,其包含黑点数的最小值与最大值之间的所有点数目都能够取得到。
简单证明一下:考虑一个连通子图删除一个点再加入一个点后,黑点的数目变化最多只为1。因此可以变化到$[min,max]$之间所有的数目。
然后就可以使用树形背包dp。设$f[i][j]$表示从$i$的子树中选出大小为$j$的连通子图,黑点数目的最小值;$g[i][j]$表示黑点数目的最大值。然后树形背包转移即可。
注意树形背包的正确枚举姿势:只使用已经遍历过的点数目和当前子树中的点数目转移,否则会被链卡到$O(n^3)$。
时间复杂度$O(Tn^2)$
#include <cstdio> #include <cstring> #include <algorithm> #define N 5010 using namespace std; int n , head[N] , to[N << 1] , next[N << 1] , cnt , v[N] , si[N] , f[N][N] , g[N][N]; inline void add(int x , int y) { to[++cnt] = y , next[cnt] = head[x] , head[x] = cnt; } void dfs(int x , int fa) { int i , j , k; si[x] = 1 , f[x][1] = g[x][1] = v[x]; for(i = head[x] ; i ; i = next[i]) { if(to[i] != fa) { dfs(to[i] , x); for(j = si[x] ; j ; j -- ) for(k = si[to[i]] ; k ; k -- ) f[x][j + k] = min(f[x][j + k] , f[x][j] + f[to[i]][k]) , g[x][j + k] = max(g[x][j + k] , g[x][j] + g[to[i]][k]); si[x] += si[to[i]]; } } for(i = 1 ; i <= n ; i ++ ) f[0][i] = min(f[0][i] , f[x][i]) , g[0][i] = max(g[0][i] , g[x][i]); } int main() { int T; scanf("%d" , &T); while(T -- ) { memset(head , 0 , sizeof(head)) , memset(f , 0x3f , sizeof(f)) , memset(g , 0xc0 , sizeof(g)) , cnt = 0; int m , i , x , y; scanf("%d%d" , &n , &m); for(i = 1 ; i < n ; i ++ ) scanf("%d%d" , &x , &y) , add(x , y) , add(y , x); for(i = 1 ; i <= n ; i ++ ) scanf("%d" , &v[i]); dfs(1 , 0); for(i = 1 ; i <= m ; i ++ ) { scanf("%d%d" , &x , &y); if(y >= f[0][x] && y <= g[0][x]) puts("YES"); else puts("NO"); } puts(""); } return 0; }