【学习笔记】基环树dp
NCC79601
2019-05-17 17:22:33
## 例题 [P2515](https://www.luogu.org/problemnew/show/P2515)
分析这道题,每个软件$i$依赖于$d[i]$,那么就可以从$d[i]$向$i$连一条边,表示只有装了$d[i]$后,装$i$才有意义。由于每个软件最多依赖一个不为自己的软件,因此,每个点的入度最大为$1$,给定的软件就构成了一片**基环外向树森林**。比如,其中一棵树可能长这样:
[![1.th.jpg](https://img.ffis.me/images/2019/05/17/1.th.jpg)](https://img.ffis.me/image/DtbF)
就非常形象了。
那么考虑怎么对环处理,由于题意,对于环上的软件,只有在所有处在同一个环上的软件都被安装后,才能够产生价值,因此可以先用$\textbf{Tarjan}$**缩点**,这样就转化成没有环的森林了。
接下来要对森林进行$\text{dp}$,因为所有树上的软件都可能对最终答案造成影响,然而一棵树与另一棵树又保持相对独立,所以不能直接用$\text{toposort}$处理(之前就因为直接$\text{toposort}$丢了40分…)。
考虑把$0$作为**超级根**(虚点),每棵树上任意入度为$0$的点作为根,从$0$开始向所有树考虑,然后把结果汇集于$0$,这样就可以在$0$处获得答案。
具体做法为:维护$f[u][i]$,表示考虑以$u$为根节点的子树,当最大容量为$i$时的最大价值,易得转移方程为:
$$f[u][i+w[u]]=\max f[u][i+w[u]-j]+f[v][j]$$
**注意**:转移方程中,$i$应**倒序**枚举,因为答案无前效性;$j$应**顺序**枚举,因为需要让$f[u][w[u]-j]$是倒序枚举。
最后,$f[0][m]$即是答案。
链接:[有向无环图$\text{dp}$](https://ncc79601.blog.luogu.org/DAG-dp)
---
完整代码
```cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 110;
const int MAXM = 510;
int n, m;
int w[MAXN], V[MAXN], d[MAXN], _w[MAXN], _V[MAXN], _d[MAXN];
vector<int> to[MAXN], _to[MAXN];
int dfn[MAXN], low[MAXN], clr[MAXN], color = 0, s[MAXN], top = 0;
bool ins[MAXN];
int indeg[MAXN], f[MAXN][MAXM];
void tarjan(int u)
{
static int dfn_cnt = 0;
dfn[u] = low[u] = ++dfn_cnt;
s[++top] = u;
ins[u] = true;
for(int i = 0; i < to[u].size(); i++)
{
int v = to[u][i];
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
}
else
if(ins[v])
{
low[u] = min(low[u], dfn[v]);
}
}
if(dfn[u] == low[u])
{
color++;
int v;
while(v = s[top--])
{
ins[v] = false;
clr[v] = color;
_V[color] += V[v];
_w[color] += w[v];
if(v == u)
break;
}
}
return;
}
void dp(int u)
{
for(int i = _w[u]; i <= m; i++)
f[u][i] = _V[u];
for(int k = 0; k < _to[u].size(); k++)
{
int v = _to[u][k];
dp(v);
for(int i = m - _w[u]; i >= 0; i--)
for(int j = 0; j <= i; j++)
f[u][i + _w[u]] = max(f[u][i + _w[u]], f[u][i + _w[u] - j] + f[v][j]);
}
return;
}
int main()
{
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++)
scanf("%d", &w[i]);
for(int i = 1; i <= n; i++)
scanf("%d", &V[i]);
for(int i = 1; i <= n; i++)
{
scanf("%d", &d[i]);
if(d[i])
to[d[i]].push_back(i);
}
for(int i = 1; i <= n; i++)
if(!dfn[i])
tarjan(i);
for(int i = 1; i <= n; i++)
{
if(clr[i] != clr[d[i]])
{
_to[clr[d[i]]].push_back(clr[i]);
indeg[clr[i]]++;
}
}
for(int i = 1; i <= n; i++)
if(!indeg[i])
_to[0].push_back(i);
dp(0);
printf("%d", f[0][m]);
return 0;
}
```
## 例题 [P2607](https://www.luogu.org/problemnew/show/P2607)
这道题就是另一种基环树的题目了。由题目知每个点出度为$1$,不难推理出连通块中最多只存在一个环。因此可以枚举环边 $<u,v>$ 进行断边,此后整个连通块形成一棵树,就可以分别以$u$和$v$为根跑一次树形$\text{dp}$,即得到这个连通块的所有信息。
处理连通块的时候,可以采用**并查集**维护连通性,如果输入的边所连接的两点$u,v$已经处于一个连通块当中,那么这个边就是可以被去掉的**环边**,就可以将其保存下来。此后,对于所有环边依次进行断边,然后对两个端点跑$\text{dp}$,如此处理之后即可得到答案。
```cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1000010;
int fa[MAXN], n, power[MAXN];
long long f[MAXN][2];
struct EDGE
{
int to, next;
} edge[MAXN * 2];
int last[MAXN], id = 0;
int P1[MAXN], P2[MAXN], E[MAXN], cnt = 0;
bool cut[MAXN];
void build_edge(int u, int v)
{
edge[++id].to = v;
edge[id].next = last[u];
last[u] = id;
return ;
}
int getfa(int x)
{
return fa[x] == x ? x : fa[x] = getfa(fa[x]);
}
void merge(int x, int y)
{
if(getfa(x) == getfa(y))
return ;
fa[getfa(y)] = getfa(x);
return ;
}
void dfs(int u, int father)
{
f[u][0] = 0, f[u][1] = power[u];
for(int i = last[u]; i; i = edge[i].next)
{
int v = edge[i].to;
if(v == father || cut[i])
continue;
dfs(v, u);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
}
return ;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
fa[i] = i;
for (int i = 1; i <= n; i++)
{
int hate;
scanf("%d%d", &power[i], &hate);
build_edge(i, hate);
build_edge(hate, i);
if (getfa(i) == getfa(hate))
{
P1[++cnt] = i;
P2[cnt] = hate;
E[cnt] = id - 1;
// 存断边
}
else
merge(i, hate);
}
long long ans = 0;
for (int i = 1; i <= cnt; i++)
{
cut[E[i]] = cut[E[i] + 1] = true;
// 切边
dfs(P1[i], 0);
long long f1 = f[P1[i]][0];
dfs(P2[i], 0);
long long f2 = f[P2[i]][0];
ans += max(f1, f2);
cut[E[i]] = cut[E[i] + 1] = false;
}
printf("%lli", ans);
return 0;
}
```
---
## 另一道例题 [P1453](https://www.luogu.org/problemnew/show/P1453)
这道题就更简单了,保证图中只有一个环,那么就可以直接找断边,然后跑两次树形$\text{dp}$就搞定了。**注意点序起始是0还是1**,现场被坑。
```cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
int n, p[MAXN], fa[MAXN], f[MAXN][2], ans = 0;
double k;
int P1, P2, cut;
struct EDGE
{
int to, next;
} edge[MAXN * 2];
int head[MAXN], id = 0;
int getfa(int x)
{
return fa[x] == x ? x : fa[x] = getfa(fa[x]);
}
void merge(int x, int y)
{
if(getfa(x) == getfa(y))
return;
fa[getfa(y)] = getfa(x);
return ;
}
void build_edge(int u, int v)
{
edge[++id].to = v;
edge[id].next = head[u];
head[u] = id;
return ;
}
void dfs(int u, int father)
{
f[u][0] = 0, f[u][1] = p[u];
for(int i = head[u]; i; i = edge[i].next)
{
int v = edge[i].to;
if((v == father) || ((i == cut) || (i - 1 == cut)))
continue;
dfs(v, u);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
}
return;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%d", &p[i]);
fa[i] = i;
}
for (int i = 1; i <= n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
build_edge(u, v);
build_edge(v, u);
if(getfa(u) == getfa(v))
{
P1 = u, P2 = v;
cut = id - 1;
}
else
{
merge(u, v);
}
}
scanf("%lf", &k);
dfs(P1, -1);
ans = f[P1][0];
dfs(P2, -1);
ans = max(ans, f[P2][0]);
printf("%.1lf", ans * k);
return 0;
}
```