树形dp
树形dp
[[USACO 2008 Jan G]Cell Phone Network]([1003-USACO 2008 Jan G]Cell Phone Network_2021秋季算法入门班第八章习题:动态规划2 (nowcoder.com))
给你一颗树,问最少选多少个点可以把整棵树覆盖,每选一个点它会覆盖它周围和它直接相连的点。
思路:
由于一个点被覆盖有三种情况,分别是自己覆盖自己,被儿子覆盖或者被父亲覆盖。
所以不能单纯像边覆盖一样设某个点选或不选。
应该还要把不选分为被父亲覆盖还是被儿子覆盖。
\(f[i][0]\) 表示 \(i\) 选,自己覆盖自己
\(f[i][1]\) 表示 \(i\) 不选,被儿子覆盖
\(f[i][2]\) 表示 \(i\) 不选,被父亲覆盖
方程:
\(f[i][0]=\Sigma{min(f[d][0],f[d][1],f[d][2])}\) 儿子随便被谁覆盖都行
对于靠儿子 \(f[i]][1]\) 很显然会有靠哪个儿子的问题。
这里分情况讨论:
如果儿子中不选都比选少,那么就找那个选和不选差别最小的儿子来选,然后其他都在选和不选中取小的
如果儿子中有一个选了比不选少,那个贡献父亲的就选它,然后其他点都在选和不选中取小的。
\(f[i][1]=(\Sigma min(f[d][0],f[d][1]))+increse\)
这个 \(increse\) 是选的那个来贡献父亲的 \((f[d][0]-f[d][1])\)
\(f[i][2]=\Sigma min(f[d][0],f[d][1])\) 这里没有 \(f[d][2]\) 因为 \(i\) 不选
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
const int maxn = 10100;
int f[maxn][3];
vector<int> v[maxn];
void dfs(int pos,int fa)
{
f[pos][0] = 1;
f[pos][1] = 0;
f[pos][2] = 0;
int inc = INF;
for(auto d:v[pos])
{
if(d==fa) continue;
dfs(d, pos);
f[pos][0] += min(f[d][0], min(f[d][1], f[d][2]));
f[pos][1] += min(f[d][0], f[d][1]);
f[pos][2] += min(f[d][0], f[d][1]);
inc = min(f[d][0] - f[d][1], inc);
}
if(inc<0)
inc = 0;
f[pos][1] += inc;
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n - 1;i++)
{
int x, y;
cin >> x >> y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs(1, -1);
cout << min(f[1][0], f[1][1]);
}
[[CTSC1997] 选课]([P2014 CTSC1997] 选课 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 \(N\) 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 \(M\) 门课程学习,问他能获得的最大学分是多少?
输入格式
第一行有两个整数 \(N\) , \(M\) 用空格隔开。( \(1 \leq N \leq 300\) , \(1 \leq M \leq 300\) )
接下来的 \(N\) 行,第 \(I+1\) 行包含两个整数 $k_i $和 \(s_i\), \(k_i\) 表示第I门课的直接先修课,\(s_i\) 表示第I门课的学分。若 \(k_i=0\) 表示没有直接先修课(\(1 \leq {k_i} \leq N\) , \(1 \leq {s_i} \leq 20\))。
输出格式
只有一行,选 \(M\) 门课程的最大得分。
样例 #1
样例输入 #1
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
样例输出 #1
13
思路:树上背包模板题。
状态转移方程:\(f[u][j]=max(f[u][j],f[son][k]+f[u][j-k])\)
细节就是 \(f[u][j]\) 要初始化为 \(val[u]\),然后由于没有先修课的课先修课为0,所以把0作为树根,但是由于 \(f[u][j]\) 表示的是以 \(u\) 为子树的树中选了 \(j\) 节课(包括自己),所以当 \(u\) 是 \(0\) 的时候会多选了一门虚空课,所以总选课数要+1。
#include <bits/stdc++.h>
using namespace std;
int k;
int f[310][310];
vector<int> g[310];
int val[310];
void dfs(int u)
{
f[u][1] = val[u];//初始化f[u][1] 就可以了
for (auto d : g[u])
{
int to = d, w = val[d];
dfs(to);
for (int j = k + 1; j >=0; j--)
{
for (int kk = 0; kk < j; kk++)//这里必须要留位置给子树的根节点
{
f[u][j] = max(f[u][j], f[u][j - kk] + f[to][kk]);
}
}
}
}
int main()
{
int n;
cin >> n>>k;
for (int i = 1; i <= n;i++)
{
int pre, v;
cin >> pre >> val[i];
g[pre].push_back(i);
}
dfs(0);
cout << f[0][k+1];
}
dls \(O(n^2)\)写法
#include<bits/stdc++.h>
using namespace std;
const int N = 310;
const int inf = 1 << 29;
int n,q;
int sz[N], a[N], dp[N][N];
vector<int> g[N];
/*
in:
5 8
1 1 2 2
10 -1 4 10 -5
1 1
1 2
1 3
1 4
1 5
2 1
2 2
2 3
out:
10
14
19
23
18
-1
9
4
*/
void dfs(int u)
{
sz[u] = 0;
static int tmp[N];
for(auto d:g[u])
{
dfs(d);
for (int i = 0; i <= sz[u] + sz[d];i++)
tmp[i] = -inf;
for (int i = 0; i <= sz[u];i++)
{
for (int j = 0; j <= sz[d];j++)
{
tmp[i + j] = max(tmp[i + j], dp[u][i] + dp[d][j]);
}
}
sz[u] += sz[d];
for (int i = 0; i <= sz[u] + sz[d];i++)
dp[u][i] = tmp[i];
}
if(u!=0)
{
sz[u] += 1;
for (int i = sz[u]; i >= 1; i--)
{
dp[u][i] = dp[u][i - 1] + a[u];
}
dp[u][0] = 0;
}
}
int main()
{
scanf("%d%d", &n,&q);
for(int i=1;i<=n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
a[i]=y;
g[x].push_back(i);
}
dfs(0);
printf("%d",dp[0][q]);
}
求树上两点最长距离板子
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 100005;
ll f[N];
vector<int> g[N];
ll val[N];
const ll INF = 0x8f8f8f8f;
ll res = -INF;
int n;
void dfs(int u, int fa)
{
ll mx1 = 0, mx2 = 0;
for(auto d:g[u])
{
if(d==fa)
continue;
dfs(d, u);
if(mx1<f[d])
mx2 = mx1, mx1 = f[d];
else if(mx2<f[d])
mx2 = f[d];
}
res = max(res, mx1 + mx2 + val[u]);
f[u] = mx1 + val[u];
}
signed main()
{
scanf("%d", &n);
for (int i = 1; i <= n;i++)
{
scanf("%lld", &val[i]);
}
for (int i = 1; i < n; i++)
{
ll u, v;
scanf("%lld %lld", &u, &v);
g[u].push_back(v), g[v].push_back(u); //默认权值为1
}
dfs(1, -1); //以1为树的根,所以1没有父节点,因此赋为-1
printf("%lld\n", res);
}
Tree(牛客)
懒得写了,直接看题解)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6 + 5;
const int MOD = 1e9 + 7;
ll f[N];
ll ans[N];
vector<int> g[N];
ll poww(ll x, int y)
{
ll ans = 1, base = x;
while (y)
{
if (y & 1)
ans = (ans * base) % MOD;
base = (base * base) % MOD;
y >>= 1;
}
return ans;
}
void dfs1(int u,int fa)
{
f[u] = 1;
for(auto d:g[u])
{
if(d==fa)
continue;
dfs1(d,u);
(f[u] *= (f[d] + 1))%=MOD;
}
}
void dfs2(int u,int fa)
{
for(auto d:g[u])
{
if(d==fa)
continue;
if ((f[d] + 1) % MOD == 0)
{
dfs1(d, -1);
ans[d] = f[d];
continue;
}
ans[d] = (f[d] * ((ans[u] * poww(f[d] + 1, MOD - 2) % MOD + 1) % MOD))%MOD;
dfs2(d, u);
}
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n - 1;i++)
{
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
dfs1(1, -1);
ans[1] = f[1];
dfs2(1, -1);
for (int i = 1; i <= n;i++)
{
cout << ans[i] << endl;
}
}
树上mex(省赛被卡题)
ACM—河南省赛 J 树上mex - 知乎 (zhihu.com)
其实只要把树建对了,思路就很显然了。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
vector<int> g[N];
int ans[N];
int sze[N];
int val[N];
int minn[N];
int n;
void dfs(int u,int fa)
{
sze[u] = 1;
minn[u] = 0x3f3f3f3f;
for(auto d:g[u])
{
if(d==fa)
continue;
dfs(d, u);
sze[u] += sze[d];
minn[u] = min(minn[u], minn[d]);
}
if(u==0){
int maxn = 0;
for(auto d:g[u])
{
maxn = max(maxn, sze[d]);
//cout << sze[d] << "sze" << endl;
}
ans[u] = maxn;
return;
}
if(minn[u]>u)
ans[u] = n - sze[u];
else
{
ans[u] = 0;
//cout << "fu" << endl;
}
minn[u] = min(u, minn[u]);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n;i++)
scanf("%d", &val[i]);
for (int i = 2; i <= n;i++)
{
int fa;
scanf("%d", &fa);
g[val[fa]].push_back(val[i]);
g[val[i]].push_back(val[fa]);
}
dfs(0, -1);
for (int i = 0; i < n;i++)
printf("%d ", ans[i]);
printf("%d\n",n);
}
[HAOI2015] 树上染色(典题)
题目描述
有一棵点数为 \(n\) 的树,树边有边权。给你一个在 \(0 \sim n\) 之内的正整数 \(k\) ,你要在这棵树中选择 \(k\) 个点,将其染成黑色,并将其他 的 \(n-k\) 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的收益。问受益最大值是多少。
输入格式
第一行包含两个整数 \(n,k\)。
第二到 \(n\) 行每行三个正整数 \(u, v, w\),表示该树中存在一条长度为 \(w\) 的边 \((u, b)\)。输入保证所有点之间是联通的。
输出格式
输出一个正整数,表示收益的最大值。
样例 #1
样例输入 #1
3 1
1 2 1
1 3 2
样例输出 #1
3
提示
对于 \(100\%\) 的数据,\(0 \leq n,k \leq 2000\)。
思路:
第一次遇到这种树上要求两两点之间的距离和。
这种要算点对之间路径的长度和的题,难以统计每个点的贡献.这个时候一般考虑算每一条边贡献了哪些点对.
知道这个套路以后,那么这题就很好做了.
状态:设\(dp[u][i]\)表示 \(u\) 节点(子树里有 \(i\) 个黑点)的子树的边的贡献的和.
转移:转移就很好想了,知道 \(v\) 内的黑点个数 \(j\) ,知道 \(v\) 内的白点数目 \(sz[v]−j\) ,知道总共的黑点数目 \(m\) ,知道总共的白点数目 \((n−m)\) ,知道边权 \(w\) ,那么转移方程显然就是:
\(dp[u][i]=max(dp[v][j]+w∗(m−j)∗j+w∗(sz[v]−j)∗[n−m−(sz[v]−j)]+dp[u][i-j])\) \(v\) 是 \(u\) 的儿子
有点类似于树形背包的思想,一个一个的加入考虑的子树。

比如当前考虑到第三个子树,里面选了 j 个黑点,那么方程的意思其实是在前面的子树里选了 i-j 个黑点,后面没考虑到的子树没有选黑点的最大权值和,这还是背包思想,而慢慢加入子树考虑的写法会让复杂度降为 \(O(n^2)\)。说到底就是道树形背包题。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 2010;
ll f[N][N];
ll tmp[N];
vector<pair<ll, ll>> g[N];
int sz[N];
int n, k;
void dfs(int u,int fa)
{
f[u][0] = f[u][1] = 0;//对叶子的初始化
sz[u] = 1;
for(auto d:g[u])
{
ll v = d.first, w = d.second;
if(v==fa)
continue;
dfs(v, u);
sz[u] += sz[v];
for (int i = 0; i <= k;i++)
tmp[i] = f[u][i];
for (int i = 0; i <=k; i++)
{
for (int j = 0; j <= min(sz[v], i); j++)
{
if(tmp[i-j]==-1)//一些不存在的状态处理
continue;
ll res = f[v][j] + j * (k - j) * w + (sz[v] - j) * (n - k - sz[v] + j) * w;
f[u][i] = max(f[u][i], res+tmp[i-j]);
}
}
}
}
int main()
{
memset(f, -1, sizeof(f));
cin >> n >> k;
for (int i = 1; i < n;i++)
{
int u, v, w;
cin >> u >> v >> w;
g[u].push_back(make_pair(v, w));
g[v].push_back(make_pair(u, w));
}
dfs(1, 0);
cout << f[1][k] << endl;
}

浙公网安备 33010602011771号