description
有N个房间,依次标号为1~N号,每个房间中都有一定的金币,现在你需要做的就是到房间里去抢金币。
有些房间是被依次嵌套或包含的,每个房间u都有一个父亲房间f,意思是u直接被f嵌套,而f成为u的父房间,u成为f的子房间,若f为0,表示当前的u为最外层的房间,1号房间一定是最外层的房间。
你具有超能力,能够在任意时刻进入任何一个房间,然后把这个房间的金币一次性拿光(你不拿光的话钱也就没了,这属于一种超自然现象),每个房间只能进入一次。
但是你的超能力有以下2个限制:
1. 但当你进入1号房间时,你就不能再进入其他房间了;
2. 任意时刻,当一个房间至少有2个子房间被你进入过,且这个房间之前没有进入过,那么下一次就必须进入这个房间;
请你根据以上信息,求最多能拿到的金币的数目。
input:
数据的第一行包含一个正整数T(1 <= T <= 100),表示有T组测试数据。
对于每组测试数据,第一行包含一个正整数N(1 <= N <= 100000),表示有N个房间。
第二行包含N个正整数Wi(1 <= Wi <= 10000),表示第i个房间有Wi个金币。
第三行包含N个整数Fi(0 <= Fi <= N),表示第i个房间被第Fi的房间所包含,若Fi = 0,说明是最外面的房间,数据保证F1 = 0
output:
对于每组测试数据,首先输出样例数,格式为”Case #X: “,其中X表示当前样例数,从1开始递增,具体信息可参见Sample Output。
然后输出一个正整数,表示最多能拿到的金币的数目。
思路:
明显的树形结构,仔细分析”超能力“的限制条件,可以发现必定存在一种临界的状态,使得一旦进入最深的一颗子树,便立刻不断回溯到其父亲节点,直到节点1。为了使总的金币数量最多,这种临界状态下的子树上的每一个节点的另一颗子树需要保证全部选取。这样一来对于整体树上的每一个节点就有两种状态,一种是从当前节点进入,依次进入其子节点,类似于先根遍历,可以在符合限制的情况下完整的遍历整个子树。这种状态用dp[1]来保存,将其命名为满树。另一种状态即为临界状态,姑且将这种状态称为半空树并以dp[2]来保存,对于以某一节点为根的半空树,他的特点是其子树中只有一颗为满树,还有一颗为半空树,剩余的子树均为一种新的状态dp[0],称为空树。空树与半空树类似,只不过空树的子树中只有一颗满树,其余的均为空树。空树与半空树的区别在于半空树在子节点构造完成后,需要加上自己节点的权值,因为半空树并定满足超能力限制第二条,进入父亲节点的房间,而空树不需要,也就是说空树的根节点不选。
由上面的状态的定义和限制可知同一颗子树,其满树状态 > 半空树状态 > 空树状态
dp[i][1] += dp[k][1]
dp[i][0] = max(dp[k][1] + sum(dp[rest][0])
dp[i][2] = max(dp[k][1] + dp[m][2] + sum(dp[rest][0]))
由转移方程可以知道,先dfs计算满树,再dfs计算空树,最后计算半空树。其中半空树的构造最为复杂,需要处理最大半空树和满树相同的情况,需要保存每个节点第二大的半空子树,以及第二大的满子树。
因为题目限制,不可能是满树的状态从1号节点退出,只能是半空树的状态。
注意:题中可能不止一颗以1号节点为根的树,若有多根,其余跟均去满树状态。
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
const int size = 200000;
int n;
int value[size];
int dp[size][3];
int maxdi[size];
int maxind[size];
int secdi[size];
std::vector<int> v[size];
inline void read(int &x){
char c=0;
for(;c<'0'||c>'9';c=getchar());
for(x=0;c>='0'&&c<='9';c=getchar())x=x*10+(c-'0');
}
void initial()
{
memset(dp, 0, sizeof(dp));
memset(maxdi, 0, sizeof(maxdi));
memset(secdi, 0, sizeof(secdi));
for(int i = 0; i <= n; i++)
v[i].clear();
}
void dfs1(int a)//计算满树状态
{
for(int i = 0; i < v[a].size(); i++)
{
dfs1(v[a][i]);
dp[a][1] += dp[v[a][i]][1];
}
dp[a][1] += value[a];
}
void dfs0(int a)//计算空树状态
{
int total = 0;
//如果可能有负数就不能定义为0,此处满子树的值一定大于部分空子树的值。
for(int k = 0; k < v[a].size(); k++)
{
dfs0(v[a][k]);
total += dp[v[a][k]][0];
int di = dp[v[a][k]][1] - dp[v[a][k]][0];
if(di > maxdi[a])
{
secdi[a] = maxdi[a];
maxdi[a] = di;
maxind[a] = k;
}else
secdi[a] = max(secdi[a],di);
}
dp[a][0] = total + maxdi[a];
}
void dfs2(int a)//计算半空树状态
{
int total = 0;
int mm = 0;
int m2 = 0;
int m2in;
int se2 = 0;
for(int i = 0; i < v[a].size();i++)
{
total += dp[v[a][i]][0];
dfs2(v[a][i]);
int di = dp[v[a][i]][2] - dp[v[a][i]][0];
if(di > m2)
{
se2 = m2;
m2 = di;
m2in = i;
}else
se2 = max(se2, di);
}
if(v[a].size() == 1)//特殊处理子节点只有一个的情况
{
dp[a][2] = dp[v[a][0]][1] + value[a];
return;
}
if(maxind[a] != m2in)
mm = maxdi[a] + m2;
else
mm = max(secdi[a] + m2, maxdi[a] + se2);
dp[a][2] = total + mm + value[a];
}
int main()
{
// freopen("0.txt","r",stdin);
// freopen("lt.txt","w",stdout);
int t;
read(t);
for(int i = 1; i <= t; i++)
{
read(n);
initial();
for(int j = 1; j <= n; j++)
read(value[j]);
for(int j = 1; j <= n; j++)
{
int a;
read(a);
v[a].push_back(j);
}
int ans = 0;
for(int j = 0; j < v[0].size(); j++)
{
dfs1(v[0][j]);
if(v[0][j] == 1)
continue;
ans += dp[v[0][j]][1];
}
dfs0(1);
dfs2(1);
printf("Case #%d: %d\n",i,ans + dp[1][2]);//取1节点的半空树状态以及其他根节点的满树状态
}
return 0;
}