【牛客题单】动态规划课程树型dp习题
NC13249 黑白树
大意:
一棵n个点的有根树,1号点为根,相邻的两个节点之间的距离为1。树上每个节点i对应一个值k[i]。每个点都有一个颜色,初始的时候所有点都是白色的。你需要通过一系列操作使得最终每个点变成黑色。每次操作需要选择一个节点i,i必须是白色的,然后i到根的链上(包括节点i与根)所有与节点i距离小于k[i]的点都会变黑,已经是黑的点保持为黑。问最少使用几次操作能把整棵树变黑。
思路:
首先需要明确,处理某个节点的时候,它的子节点一定都全部染成黑色了
dfs的时候传递的是:操作res-1次时,子节点能染色的最大范围,而操作res次时,染色的最大范围则挂在k数组上更新,这样相当于进行了两个数值的传递
所以当子节点传递上来的范围不包括当前节点时,就需要进行染色,但是不一定是染这个节点,只是选择一个能贡献最大染色范围的节点
但是我们不需要考虑到底是染哪个点,只需要把最大的染色范围更新到当前的k数组即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
typedef long long LL;
int n,f[N],res=0;
vector<int> mp[N];
int dfs(int now, int fa) {
int w = 0;
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i];
if (ne == fa) continue;
w = max(w, dfs(ne, now));
}
if(w<=0){
res++;
return f[now] - 1;
}
f[fa] = max(f[fa], f[now] - 1);
return w-1;
}
int main() {
cin >> n;
for (int i = 2; i <= n; i++) {
int x;
cin >> x;
mp[x].push_back(i);
}
for (int i = 1; i <= n; i++) cin >> f[i];
dfs(1, 0);
cout << res << endl;
return 0;
}
NC15748 旅游
大意:
旅行地图上有n个城市,它们之间通过n-1条道路联通。
Cwbc和XHRlyb第一天会在s市住宿,并游览与它距离不超过1的所有城市,之后的每天会选择一个城市住宿,然后游览与它距离不超过1的所有城市。他们不想住在一个已经浏览过的城市,又想尽可能多的延长旅行时间。XHRlyb想知道她与Cwbc最多能度过多少天的时光呢?
思路:
最大点独立集裸题
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
typedef long long LL;
int n, s,dp[N][2];
vector<int> mp[N];
void dfs(int now, int fa) {
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i];
if (ne == fa) continue;
dfs(ne, now);
dp[now][1] += dp[ne][0];
dp[now][0] += max(dp[ne][0], dp[ne][1]);
}
dp[now][1]++;
}
int main() {
cin >> n >> s;
for (int i = 0; i < n - 1; i++) {
int x, y;
cin >> x >> y;
mp[x].push_back(y);
mp[y].push_back(x);
}
dfs(s, 0);
cout << dp[s][1] << endl;
return 0;
}
NC19782 Tree
大意:
对于n个节点的一个树中的每一个点,求出包含这个点的点联通集的数量
思路:
换根就要想到把联通集分为两类,一类是这个点为根的子树中的联通集,另一类是不以它为根的
设dp[ i ]为以i为根的子树中包含i这个点的点联通集的数量,ne为i的子节点,那么:
\(dp[i]=\prod(dp[ne]+1)\)
即每个子节点选(dp[ne])或不选(1),然后相乘
设pre[i]为除了i的子树中包含i号点的联通集的数量,假设res[ i ]为每个点的答案,fa为父节点,那么有:
\(pre[i]=\frac{res[fa]}{dp[i]+1}\)
因为res[ fa ]代表父节点的答案,也就是所有包含父节点的联通集,可以由父节点以上的联通块再乘上i节点选或不选,
即res[fa] =pre[i] * (dp[i]+1),那么直接除一下得到pre[i]
但是是取模意义下的,所以要求逆元
但是如果dp[i]+1对mod取模为0,那么无法直接求逆元来算pre,此时只需要反向考虑,暴力求一下pre即可
总结一下就是务必要搞清楚各个数组的含义,不然很难推出传递关系。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
typedef long long LL;
LL qmi(LL a, LL k, LL p) {
LL res = 1 % p; // res记录答案, 模上p是为了防止k为0,p为1的特殊情况
while (k) { // 只要还有剩下位数
if (k & 1)
res =
(LL)res * a % p; // 判断最后一位是否为1,如果为1就乘上a,模上p,
// 乘法时可能爆int,所以变成long long
k >>= 1; // 右移一位
a = (LL)a * a %
p; // 当前a等于上一次的a平方,取模,平方时可能爆int,所以变成long
// long
}
return res;
}
LL get_inv(LL a, LL p) { return qmi(a, p - 2, p); }
int n;
vector<int> mp[N];
LL dp[N];
int f[N];
LL const mod = 1e9 + 7;
void dfs1(int now, int fa) {
dp[now] = 1;
f[now] = fa;
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i];
if (ne == fa) continue;
dfs1(ne, now);
dp[now] = dp[now] * (dp[ne] + 1) % mod;
}
}
LL pre[N], res[N];
void dfs2(int now, int fa) {
if (fa != 0) {
if ((dp[now] + 1) % mod) {
pre[now] = res[fa] * get_inv(dp[now] + 1LL, mod) % mod;
} else {
int tmp = pre[fa] + 1;
for (int i = 0; i < mp[fa].size(); i++) {
int ne = mp[fa][i];
if (ne == now || ne == f[fa]) continue;
tmp = tmp * (dp[ne] + 1) % mod;
}
pre[now] = tmp;
}
res[now] = dp[now] * (pre[now] + 1) % mod;
}
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i];
if (ne == fa) continue;
dfs2(ne, now);
}
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n - 1; i++) {
int x, y;
scanf("%d%d", &x, &y);
mp[x].push_back(y);
mp[y].push_back(x);
}
dfs1(1, 0);
res[1] = dp[1];
dfs2(1, 0);
for (int i = 1; i <= n; i++) printf("%lld\n", res[i]);
return 0;
}
NC20811 蓝魔法师
大意:
给出一棵树,求有多少种删边方案,使得删后的图每个连通块大小小于等于k,两种方案不同当且仅当存在一条边在一个方案中被删除,而在另一个方案中未被删除,答案对998244353取模
思路:
\(dp[i][j]\)表示i点连通块大小为j的方案数
对于每棵子树,有两种可能
1.不删除这条边,两个点相连
那么这个点的连通块大小就是两个点的连通块大小相加,对于方案数直接相乘
即\(dp[u][i+j]+=dp[u][i]∗dp[v][j]\)
2.删除这条边,两个点各自独立
\(dp[u][i]=dp[u][i]∗∑dp[v][j]\)
可以注意到这是一种 01背包,关于son选或不选的问题。
普通01背包第一层是枚举物品种类,这里即枚举第几个 son。
第二层枚举体积,这里即枚举连通块大小,故先逆序枚举root的连通块大小,然后枚举(不考虑顺序)子集连通块来更新即可。
这里需要注意在枚举 son时,保证当前的root中没有出现过son,否则会出现重复方案。
需要注意的是,本题看上去是n3的算法,但是通过size优化,复杂度可以证明是n2
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 5;
typedef long long LL;
int n, k, size[N];
vector<int> mp[N];
LL dp[N][N];
LL const mod = 998244353;
void dfs(int now, int fa) {
size[now] = 1;
dp[now][1] = 1;
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i];
if (ne == fa) continue;
dfs(ne, now);
LL sumne = 0;
for (int j = 1; j <= min(k, size[ne]); j++) {
sumne = (sumne + dp[ne][j]) % mod;
}
for (int j = min(k, size[now]); j >= 1; j--) {
for (int l = min(k, size[ne]); l >= 1; l--) {
if (j + l <= k) {
dp[now][j + l] =
(dp[now][j + l] + dp[now][j] * dp[ne][l] % mod) % mod;
}
}
dp[now][j] = (dp[now][j] * sumne) % mod;
}
size[now] += size[ne];
}
}
int main() {
cin >> n >> k;
for (int i = 0; i < n - 1; i++) {
int x, y;
cin >> x >> y;
mp[x].push_back(y), mp[y].push_back(x);
}
dfs(1, 0);
LL res = 0;
for (int i = 1; i <= k; i++) {
res = (res + dp[1][i]) % mod;
}
cout << res << endl;
return 0;
}
NC51179 选课
大意:
学校开设了 N 门的选修课程,每个学生可选课程的数量 M 是给定的。
在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其他的一些课程的基础上才能选修。每门课的直接先修课最多只有一门。两门课可能存在相同的先修课。
你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修条件。
思路:
比较裸的树上01背包,对于没有先导课的课程,挂到0号节点,然后从0号节点开始dfs即可
注意m需要+1,这样选择m+1门课,因为0号点必须要选
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 5;
typedef long long LL;
int n, size[N], w[N],m;
vector<int> mp[N];
LL dp[N][N];
void dfs(int now, int fa) {
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i];
if (ne == fa) continue;
dfs(ne, now);
for (int j = m-1; j >= 0; j--) {
for (int k = 0; k <= j; k++) {
dp[now][j] = max(dp[now][j], dp[now][j - k] + dp[ne][k]);
}
}
}
for (int i = m; i >= 1; i--) dp[now][i] = dp[now][i - 1] + w[now];
dp[now][0] = 0;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int x;
cin >> x >> w[i];
mp[x].push_back(i);
}
m++;//因为0号点必选,所以需要选m+1个点
dfs(0, -1);
cout << dp[0][m] << endl;
return 0;
}
NC51180 Accumulation Degree
大意:
给出一个树,每条边有权值w,表示容量。题目要求的是,以某个点为根,计算根到所有叶子节点的流量和,最后再取最大值。
思路:
换根dp,设\(dp [ i ][0]\)为从i点往子节点流可以流出的最大流量,\(dp[i][1]\)为流到其他所有点的最大流量,也就是当前点的答案
\(dp[i][0]\)肯定等于每个子树的贡献和,每个子树怎么算贡献呢,如果子树是叶子节点,那么贡献是这条边的容量,因为可以全部流过去
如果子树不是叶子节点,那么贡献是\(min(c,dp[ne][0])\),很好理解,就是子树能流出的最大值,然后和容量取min
第一次dfs求出\(dp[i][0]\)后,再考虑求\(dp[i][1]\)
对于一个点来说,流向其他点的全部流量,必然等于流向子节点的流量和加上流向父节点的流量,流向子节点的流量和为\(dp[i][0]\)
而流向父节点的流量是父节点流向除了这个点以外的其他点的全部流量(设为g),而父节点流向其他节点的全部流量已经算出来了(\(dp[i][1]\)),把能流向这个节点的流量减去即可得到g,能流向这个节点的流量就是上文提到的这个节点的贡献
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
typedef long long LL;
int n;
vector<pair<int,LL> > mp[N];
LL dp[N][2], w[N],pre[N];
//0:可以流出的最大值
//1:答案
void dfs1(int now, int fa,LL c) {
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i].first;
LL w = mp[now][i].second;
if (ne == fa) continue;
dfs1(ne, now,w);
if (mp[ne].size() == 1) dp[now][0] += w;
else dp[now][0] += min(w,dp[ne][0]);
}
}
void dfs2(int now, int fa,LL c) {
if(fa){
if(mp[now].size()==1) dp[now][1] = dp[now][0];
else dp[now][1] = dp[now][0]+min(c,dp[fa][1]-min(c,dp[now][0]));
}
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i].first;
LL w = mp[now][i].second;
if (ne == fa) continue;
dfs2(ne, now,w);
}
}
int main() {
int t;
cin >> t;
while (t--) {
cin >> n;
for (int i = 1; i <= n; i++) mp[i].clear();
memset(dp, 0, sizeof dp);
for (int i = 0; i < n - 1; i++) {
int x, y;
LL w;
cin >> x >> y >> w;
mp[x].push_back({y, w}), mp[y].push_back({x, w});
}
dfs1(1, 0,0x3f3f3f3f3f3f3f);
dp[1][1] = dp[1][0];
dfs2(1, 0,0);
LL ans = 0;
for (int i = 1; i <= n; i++)
ans = max(ans, dp[i][1]);
cout << ans << endl;
}
return 0;
}
NC200547 划分树
大意:
给出一棵 n 个点的树,点编号 1..n , i 号点的点权是 ai, 可以通过删边的方式将这棵树划分成一些连通块,求有多少种不同的划分方案,满足:划分后每个连通块的点权异或和均为 M 。 答案对 1004535809 取模。
思路:
看不懂...丢个题解吧:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
typedef long long LL;
int n;
vector<pair<int,LL> > mp[N];
LL dp[N][2], w[N],pre[N];
//0:可以流出的最大值
//1:答案
void dfs1(int now, int fa,LL c) {
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i].first;
LL w = mp[now][i].second;
if (ne == fa) continue;
dfs1(ne, now,w);
if (mp[ne].size() == 1) dp[now][0] += w;
else dp[now][0] += min(w,dp[ne][0]);
}
}
void dfs2(int now, int fa,LL c) {
if(fa){
if(mp[now].size()==1) dp[now][1] = dp[now][0];
else dp[now][1] = dp[now][0]+min(c,dp[fa][1]-min(c,dp[now][0]));
}
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i].first;
LL w = mp[now][i].second;
if (ne == fa) continue;
dfs2(ne, now,w);
}
}
int main() {
int t;
cin >> t;
while (t--) {
cin >> n;
for (int i = 1; i <= n; i++) mp[i].clear();
memset(dp, 0, sizeof dp);
for (int i = 0; i < n - 1; i++) {
int x, y;
LL w;
cin >> x >> y >> w;
mp[x].push_back({y, w}), mp[y].push_back({x, w});
}
dfs1(1, 0,0x3f3f3f3f3f3f3f);
dp[1][1] = dp[1][0];
dfs2(1, 0,0);
LL ans = 0;
for (int i = 1; i <= n; i++)
ans = max(ans, dp[i][1]);
cout << ans << endl;
}
return 0;
}
NC201400 树学
大意:
设一棵树的权值为每个点的深度和,试着通过选择树的根,使得这棵树的权值最小,输出这个权值
思路:
换根dp,第一次统计当根为1时,每个点的深度、子树大小和贡献,第二次dfs时换根即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
typedef long long LL;
#define int LL
int n,size[N];
vector<int> mp[N];
int dp[N][2],deep[N];
void dfs1(int now, int fa) {
deep[now] = deep[fa] + 1;
dp[now][0] += deep[now];
size[now] = 1;
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i];
if (ne == fa) continue;
dfs1(ne, now);
dp[now][0] += dp[ne][0];
size[now] += size[ne];
}
}
void dfs2(int now, int fa) {
if(fa)
dp[now][1] = dp[now][0] - size[now] * deep[now] + dp[fa][1] -
(dp[now][0] - (size[now] * (deep[now] - 1)))+n-size[now];
for (int i = 0; i < mp[now].size(); i++) {
int ne = mp[now][i];
if (ne == fa) continue;
dfs2(ne, now);
}
}
signed main() {
cin >> n;
for (int i = 0; i < n - 1; i++) {
int x, y;
cin >> x >> y;
mp[x].push_back(y), mp[y].push_back(x);
}
deep[0] = -1;
dfs1(1, 0);
dp[1][1] = dp[1][0];
dfs2(1, 0);
int res = 0x3f3f3f3f;
for (int i = 1; i <= n; i++) res = min(res, dp[i][1]);
cout << res << endl;
return 0;
}