给定一颗有N个节点的树(一般是无根树,就有N-1条无向边),可以任选一个节点作为根节点
一般以节点从深到浅(子树从小到大)的顺序作为dp阶段顺序
dp的状态表示中,第一维通常是节点编号(节点编号代表了以该节点为根的子树)
对于每个节点x,先递归在它的每个子节点上进行dp,回溯时,从子节点向x进行状态转移
A - Anniversary part
N个员工,编号为1~N
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直属上司。现在有个宴会,宴会每邀请来一个员工 i 都会增加一定的快乐指数 Ri,但如果某个员工的直属上司来了,那么这个员工就不会来。计算邀请哪些员工可以使快乐指数最大,输出最大的快乐指数
设 dp [x] [0] 表示在 x 为根的子树中邀请部分员工,并且 x 不参加时,快乐指数总和的最大值,此时 x 子节点(直接下属)可以参加也可以不参加 (s表示x子节点)
设 dp [x] [1] 表示在 x 为根的子树中邀请部分员工,并且 x 参加时,快乐指数总和的最大值,此时 x 子节点(直接下属)都不能参加,H[x] 表示当前节点(x)的快乐指数 (s表示x子节点)
这个题给的是有根树,就可以从根节点开始,dp目标就是 max(F[root,0] , F[root,1]) 时间复杂度ON
#include
#include
#include
#include
#include
using namespace std;
vector son[10010];
int dp[10010][2];//0不参加,1参加
int v[10010];//记录有没有父节点
int h[10010];//记录快乐指数
int n;
void DFS(int x){
dp[x][0] = 0;
dp[x][1] = h[x];
for (int i = 0; i < son[x].size(); i++)
{
int y = son[x][i];
DP(y);
dp[x][0] += max(dp[y][0],dp[y][1]);
dp[x][1] += dp[y][0];
}
}
int main(){
cin>>n;
for (int i = 1; i <=n ; i++)
scanf("%d",&h[i]);
for (int i = 1; i >x>>y;
v[x] = 1;//x有爸爸
son[y].push_back(x);//x是y的子节点
}
int root;
for (int i = 1; i <= n; i++)
if(!v[i]){ //i没有爸爸
root = i;
break;
}
DFS(root);
cout << max(dp[root][0],dp[root][1]) << endl;
return 0;
}
B - Strategic game
有n个点,在某些点上放置哨兵,每个哨兵可以监控和它有边相连的点,问监视所有的点需要的最少的哨兵数
也就是说一颗n个结点的树,要求选出其中的一些顶点,使得对于树中的每条边(u, v),u和v至少有一个被选中,要求给出选中顶点数最少的方案
设 dp [x] [0] 表示在不选择节点 x 的情况下,以 x 为根节点的子树,最少需要选择的节点数
设 dp [x] [1] 表示在选择节点 x 的情况下,以 x 为根节点的子树,最少需要选择的节点数
当i为叶子节点时
#include
#include
#include
#include
#define maxn 1508
using namespace std;
int dp[maxn][2];
int soncnt[maxn];
int parent[maxn];
int n;
void DFS(int x) {
int i, d1=0, d0=0;
if (soncnt[x] == 0) {
dp[x][0] = 0;
dp[x][1] = 1;
return;
}
for (i=0; i < n; i++) {
if (parent[i] == x) {
DFS(i);
d1 += min(dp[i][0], dp[i][1]);
d0 += dp[i][1];
}
}
dp[x][1] = d1 + 1;
dp[x][0] = d0;
}
int main() {
int dad, son, m;
while (cin >> n) {
memset(soncnt, 0, sizeof(soncnt));
memset(parent, -1, sizeof(parent));
int root = -1;
for (int i = 0; i < n; i++) {
scanf("%d:(%d)", &dad, &m);
soncnt[dad] = m;
if (root == -1) {
root = dad;
}
while (m--) {
scanf("%d", &son);
parent[son] = dad;
}
}
DFS(root);
cout << min(dp[root][0], dp[root][1]) << endl;
}
return 0;
}
C - Tree Cutting
给一颗n个结点的树,节点编号为1~n
问:删除哪些结点后,剩余各个子树的大小均小于原总结点数的一半
拆除一个节点后,剩余部分为其若干儿子的子树以及该节点上层所连其余部分(n-size[i]),只要这些连接块大小都不超过n/2,该节点就满足条件。因而我们可以先求出每个节点所管辖的那棵树的大小,自下而上地为每个节点求出其子树规模(该点规模=其儿子的规模和+1)。
在建树的时候可以直接用连接表(vector)储存无向边,这时由于无法区分与每个点相连的是其父节点还是子节点,会引发问题:在dfs的时候把父节点误认作儿子节点,解决方法就是在递归的时候传入父节点编号,然后在递归,计算规模,比较大小的时候避开它就可以了
#include
#include
#include
#include
#include
using namespace std;
int n;
int root;
vectorG[10000+400];
vectorans;
int sz[10000+400];
void dfs(int par,int u){
sz[u]=1;
for(int i=0;i<(int)G[u].size();i++){
if(G[u][i]!=par)
dfs(u,G[u][i]);
}
int piece=0;
for(int i=0;i<(int)G[u].size();i++)
if(par!=G[u][i]){
sz[u]+=sz[G[u][i]];
piece=max(piece,sz[G[u][i]]);
}
piece=max(piece,n-sz[u]);
if(piece<=n/2)
ans.push_back(u);
}
int main(){
while(cin>>n){
memset(sz,0,sizeof(sz));
int x,y;
for(int i=0;i
LCA 最近公共祖先
倍增(基于二分的方法)
假如树上的两个点,处于同一深度时,对于往上跳mid步,显然有单调性:
如果它们往上跳mid步是同一个点,则这个点是它们的共同祖先,但不一定是最近公共祖先
那么如果我们可以快速得到每个点往上跳若干步是谁,显然我们可以利用二分来求LCA
这个可以用倍增来做。
倍增的思想其实非常简单,就是利用二进制的思想。一个显然的结论有:
节点u往上跳2^(k+1)的布的祖先 = (节点u往上跳2^k布的祖先)往上跳2^k布的祖先
用代码表示就是:
fa[u][k + 1] = fa[ fa[u][k] ][k];
这个的预处理也非常简单,我们只需要初始化每个fa[u][0]
就可以了就可以了,因为接下来的部分都可以根据上面的公式递推。
然后预处理好这个之后,我们就可以根据这个进行二分了,代码如下:
int fa[maxn][20], dep[maxn];
void dfs(int u, int f, int d)
{
dep[u] = d;
fa[u][0] = f;
for(int i = 1; i < 20; ++i)
{
fa[u][i] = fa[fa[u][i - 1]][i] - 1;
}
for(int i = head[u]; ~i; i = nxt[i])
{
int t = to[i];
if(t != f)
{
dfs(t, u, d + 1);
}
}
}
int lca(int u, int v)
{
if(dep[u] < dep[v]) swap(u, v);
int k = dep[u] - dep[v];
for(int i = 0; i < k; ++i)
{
if((1 << i) & k) u = fa[u][i];
}
if(u == v) return u;
for(int i = 19; i >= 0; --i)
{
if(fa[u][i] != fa[v][i])
{
u = fa[u][i];
v = fa[v][i];
}
}
return fa[u][0];
}