树形依赖背包
一般形式:给定一颗(n)个节点的点权树,要求选出(m)个节点使得这些选出的节点的点权和最大,一个节点能选当且仅当其父亲节点被选中,根节点能直接选。
一般解法:
用(f_{u,i})表示在(u)的子树中选择(i)个节点(包括本身)的最大价值,转移方程为:$$f_{u,i} = max(f_{u,j} + f_{v, i - j} + d_v) [j = 1 ... i - 1]$$
其中(d_v)表示(v)的点权,(i-j)表示在子树(v)中选择(i - j)个节点。
遍历复杂度(O(n)),总复杂度(O(nm^2))
优化:
一般有两种方式可以优化到(O(nm))
-
树的孩子兄弟表示法:一种将多叉树变为二叉树的常用方法,就是将每个点与它的第一个儿子连边,然后将它的儿子依次连接起来,如图:
设(f_{i,j})为以(i) 为根的子树中用大小为(j)的包能取到的最大价值,那么转移方程为:$$f_{i, j} = max(f_{left[i],j-w[i]} + v[i], f_{right[i],j})$$
其中,(left_i)为(i)在原树中的第一个儿子(即二叉树中的左儿子),(right_i)为(i)在原树中的下一个兄弟(即二叉树中的右儿子)
-
DFS序法:对整棵树求出(DFS)序与子树大小(siz),那么若根节点为(u),第一个儿子即为(dfn_u + 1),第二个儿子为(dfn_u + siz_{firstson} + 1)。
设(f_{i,j})为当前DP到(DFS)序为(i)的点,目前已选(j)个点,则转移方程为:
-
选当前点:(f_{i + 1,j + 1} = f_{i,j} + d_i)
因为(i+1)号节点为(i)的儿子或者兄弟,在选(i)之后都是可选的
-
不选当前节点:(f_{nx[i],j} = f_{i, j})
其中(nx[i])表示下一颗子树,因为没有选(i),所以不能选(i)的子节点
-
以上优化都是将转移降到了(O(1)),但它们只适用于点权问题。
分组树形背包
此时,父亲与儿子之间并不存在依赖关系,我们设(f_{k,i,j})为以(i)为根的子树,在前(t)个儿子中,分离出一个大小为(j)的子树的最小代价,则对于每一个儿子(v):
其中,(fullson[v])表示(v)的儿子个数。
有了这个转移方程,我们就可以在(DFS)时DP了。
不过,这样的空间开销太大了,我们可以参照01背包的降维优化,通过逆序枚举(j)来把(t)那一维消掉
初始化时,将(f_{i,1} = ind[i]),其中,(ind_i)为与(i)有连边的节点数。因为将两个点合并到一个点集中时每一个点都会少一条出边,所以要(-2)。
参考代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 1e4 + 10;
int f[200][200],n,head[maxn],num,ind[maxn],K;
struct Edge{
int then,to;
}e[maxn];
void add(int u, int v){e[++num] = (Edge){head[u], v}; head[u] = num;}
void DFS(int u, int fa){
f[u][1] = ind[u];
for(int i = head[u]; i; i = e[i].then){
int v = e[i].to;
if(v == fa) continue;
DFS(v, u);
for(int j = K; j; -- j)
for(int k = 0; k <= j; ++ k)
f[u][j] = min(f[u][j], f[u][j - k] + f[v][k] - 2);
}
}
int Ans = 0x3f3f3f3f;
int main(){
scanf("%d%d", &n, &K);
for(int i = 1; i < n; ++ i){
int u,v; scanf("%d%d", &u, &v);
add(u, v); add(v, u);
ind[v]++; ind[u]++;
}
memset(f,0x3f,sizeof(f));
DFS(1, 0);
for(int i = 1; i <= n; ++ i) Ans = min(Ans, f[i][K]);
printf("%d
", Ans);
return 0;
}
不过,这一方法只适用于处理边权,用于点权就效率太低下了。
换根DP(二次扫描法)
一般来说,我们会默认(1)为树的根,但是有些题目要求计算以每一个节点为根时的内容
朴素想法是枚举每一个点作为根时的情况,复杂度(O(n^2)),显然太高了;
正解:换根DP,复杂度(O(n))。
大致思路:
- 以(1)为根(DFS)一遍,统计出所需要的数据和以(1)为根的答案;
- 从(1)开始再次(DFS),每次从节点(u)到(v)时,计算出树根从(u)转移到(v)时的贡献变化。
例题:Luogu3748 [POI2008]STA-Station
题意:给定一个 (n)个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。
一个结点的深度之定义为该节点到根的简单路径上边的数量。
思路:按照刚才的想法,我们先(DFS)一遍,求出每个节点的子树大小(siz),并求出以(1)为根的答案(f_1)。
第二遍(DFS),当根从(u)转移到(v)时,(v)子树内(含(v))节点的深度(-1),其他节点的深度(+1),所以可以得到下面的转移方程:
最后统计最大值就可以了。
参考代码:
#include <cstdio>
#define LL long long
using namespace std;
const int maxn = 1e6 + 10;
int n,head[maxn << 1],num;
LL f[maxn];
struct Edge{
int then,to;
}e[maxn << 1];
void add(int u, int v){e[++num] = (Edge){head[u], v}; head[u] = num;}
int siz[maxn];
void DFS1(int u, int fa, int deep){
siz[u] = 1;
for(int i = head[u]; i; i = e[i].then){
int v = e[i].to;
if(v == fa) continue;
DFS1(v, u, deep + 1);
siz[u] += siz[v]; f[u] += deep + 1;
}
}
void DFS2(int u, int fa){
for(int i = head[u]; i; i = e[i].then){
int v = e[i].to;
if(v == fa) continue;
f[v] = f[u] + n - 2 * siz[v];
DFS2(v, u);
}
}
int main(){
scanf("%d", &n);
for(int i = 1; i < n; ++ i){
int u,v; scanf("%d%d", &u, &v);
add(u, v); add(v, u);
}
DFS1(1, 0, 0); DFS2(1, 0);
int Max = 0;
for(int i = 1; i <= n; ++ i)
if(f[i] > f[Max]) Max = i;
printf("%d
", Max);
return 0;
}
基环树DP
一般思路:断环为链,再分类讨论
题意:给定(N)个点,(N)条边,保证任意两点间至少存在一条路径。其中每个点均有其权值(val_i),问如何选择点,使得在保证任意直接相连的两点不会同时被选中的情况下,被选中的点的权值和最大?
思路:首先,假如本题不是基环树,那么普通树形DP就可以搞定。而基环树DP的核心就是把基环树上问题转化为普通树上问题。考虑删去基环上的边(E_i),边的端点为(u),(v)。我们将(u)作为新根,可以求得(f_{u,0})与(f_{u,1})(其中(f_{i,0/1})表示以(i)为根节点的子树选/不选(i)时的最大权值和)。在实际的图中,(u)和(v)是不能同时取到的,为了保证答案合法,我们把(f_{u, 0})记为临时答案。
显然,这个答案并不能保证是最优的,因为答案可能要包含(u)。
如何解决这个问题?我们可以再以(v)作为新根,最做一遍树规,取上次的临时答案与(f_{v,0})的(max)即可。
如何保证答案最优?
当我们以(u)为根进行树规时,已经把除选择点(u)以外的最优情况求出,而当以(v)为根时,又将选择(u)的情况求出了。由于不能同时选择(u)和(v),所以答案必然最优
参考代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 1e5 + 10;
int n,f[maxn][2],head[maxn << 1],num,val[maxn];
int root,x,y;
double k;
struct Edge{
int then,to;
}e[maxn << 1];
void add(int u, int v){e[++num] = (Edge){head[u], v}; head[u] = num;}
int vis[maxn],flag;
void DFS1(int u, int fa){
vis[u] = 1;
for(int i = head[u]; i; i = e[i].then){
int v = e[i].to;
if(v == fa) continue;
if(flag) return;
if(vis[v]){
x = u, y = v; flag = 1;
return;
}
DFS1(v, u);
}
}
void DFS2(int u, int fa){
f[u][1] = val[u];
for(int i = head[u]; i; i = e[i].then){
int v = e[i].to;
if(v == fa) continue;
if((u == x && v == y) || (u == y && v == x)) continue;
DFS2(v, u);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
}
}
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i) scanf("%d", val + i);
for(int i = 1; i <= n; ++ i){
int u,v; scanf("%d%d", &u, &v);
u += 1, v += 1;
add(u, v); add(v, u);
} DFS1(1, 1);
scanf("%lf", &k);
memset(f,0,sizeof(f));
root = x, DFS2(root, root);
int Ans = f[root][0];
memset(f,0,sizeof(f));
root = y, DFS2(root, root);
Ans = max(Ans, f[root][0]);
printf("%.1lf
", Ans * k);
return 0;
}
虚树
有时候,题目会给出一颗(n)为(1e5)级别的树,每次指定(m)个节点,给它们一些性质,然后求答案,保证(sum m)与(n)为同一级别。如果我们用朴素的树形DP,复杂度是基于(n)的,会(T)到飞起。
因此,我们可以用单调栈建出一颗“虚树”,它的节点数是(m)级别的,同时又能保证答案的正确性。
建树:OI-Wiki上讲得很详细(因为略麻烦,这里不展开阐述当然不是我懒)
思路:每次建出虚树,DP即可(注意在每一次初始化时保证效率)
参考代码:
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
#define LL long long
using namespace std;
const int maxn = 6e5 + 10;
int n,m,k,now[maxn];
int head[maxn << 1],num,cur[maxn],cnt;
struct Edge{
int then,to;
LL val;
}t[maxn << 1];
vector<int> e[maxn];
void Add(int u, int v, LL val){t[++cnt] = (Edge){cur[u], v, val}; cur[u] = cnt;}
LL fa[maxn][30],dep[maxn],val[maxn],id[maxn],_num;
void DFS(int u, int f){
dep[u] = dep[f] + 1;
fa[u][0] = f; id[u] = ++_num;
for(int i = 1; i <= 18; ++ i) fa[u][i] = fa[fa[u][i - 1]][i - 1];
for(int i = cur[u]; i; i = t[i].then){
int v = t[i].to;
if(v == f) continue;
val[v] = min(val[u], t[i].val);
DFS(v, u);
}
}
int LCA(int x, int y){
if(dep[x] < dep[y]) swap(x, y);
for(int i = 18; i >= 0; -- i)
if(dep[fa[x][i]] >= dep[y]) x = fa[x][i];
if(x == y) return x;
for(int i = 18; i >= 0; -- i)
if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
bool cmp(int x, int y){return id[x] < id[y];}
int sta[maxn],top;
void build(){
sort(now + 1, now + 1 + k, cmp);
sta[1] = 1, top = 1; e[1].clear();
for(int i = 1; i <= k; ++ i){
if(top == 1){sta[++top] = now[i]; continue;}
int pos = now[i];
int L = LCA(pos, sta[top]);
if(L == sta[top]) continue;
while(id[L] <= id[sta[top - 1]] && top > 1) e[sta[top - 1]].push_back(sta[top]), top--;
if(L != sta[top]) e[L].push_back(sta[top]), sta[top] = L;
sta[++top] = pos;
}
while(top > 0) e[sta[top - 1]].push_back(sta[top]), top--;
return;
}
LL dfs(int u, int fa){
if(e[u].size() == 0) return val[u];
LL tmp = 0;
for(int i = 0; i < e[u].size(); ++ i) tmp += dfs(e[u][i], u);
e[u].clear();
return min(tmp, (LL)val[u]);
}
int main(){
scanf("%d", &n);
memset(val,0x7f,sizeof(val));
for(int i = 1; i < n; ++ i){
int u,v; LL val; scanf("%d%d%lld", &u, &v, &val);
Add(u, v, val); Add(v, u, val);
}
DFS(1, 1); scanf("%d", &m);
while(m--){
scanf("%d", &k);
for(int i = 1; i <= k; ++ i) scanf("%d", &now[i]);
build();
printf("%lld
", dfs(1, 1));
}
return 0;
}
练习题
[POI2013]LUK-Triumphal arch 题解
以上习题都较为基础,请结合自身情况使用
参考博客
http://blog.csdn.net/no1_terminator/article/details/77824790
https://www.cnblogs.com/wlzhouzhuan/p/12643056.html
https://blog.csdn.net/DorMOUSENone/article/details/54971697
https://blog.csdn.net/wu_tongtong/article/details/79219822