UVA 1218 Perfect Service (树上 DP)
本题的状态数较多,一个节点有三种状态表示(一开始我只想到两个):
0、该节点不是服务器,该节点的父节点也不是服务器
1、该节点是服务器。
2、该节点不是服务器,该节点的父节点是服务器
对于着三种状态,分别用d(u,0),d(u,1),d(u,2)表示
对于以上三种状态进行分析,并写出状态转移方程
1、对于第一种状态,则子节点里有且仅有一个为服务器。需要枚举每一个子节点为d(v,1),把其他子节点的d(v,0)相加。如果在每次枚举时进行计算,那么该操作时间复杂度为O(k2)。可以通过一个sum累计所有子节点的d(v,0),在进行枚举时,只需在sum的基础上加上一个d(v,1)再减去一个d(v,0)即可。时间复杂度降为O(k)
状态转移方程为:d(u,0)=min(d(u,0),sum+d(v,1)-d(v,0))
2、对于第二种状态,则该节点的子节点可以时服务器也可以不是服务器。
状态转移方程为:d(u,1)=
∑
m
i
n
(
d
(
v
,
1
)
,
d
(
v
,
2
)
)
\displaystyle\sum_{}^{} min(d(v,1),d(v,2))
∑min(d(v,1),d(v,2))
3、对于第三种状态,所有子节点都必须不是服务器
状态转移方程为d(u,2)=
∑
d
(
v
,
0
)
\displaystyle\sum_{}^{} d(v,0)
∑d(v,0),即为前面的sum。
初始化的值为
d(u,0)=INF ,INF最好设为比max_n多一点,以防在相加时溢出。
d(u,1)=1
d(u,2)=0
具体实现可以先将无根树转换为以1为根节点的有根树,在进行dp(但会很慢)
也可以直接用dfs从1节点遍历树,用vis数组标记访问过的节点,这样就避免了重复访问(即在子节点和父节点间无限循环),记得在对子节点进行完dfs后,将该子节点标记为未访问过,因为还要计算d(u,0)。如果不将子节点恢复为未访问,那么将无法计算。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int max_n=1e4+5;
int dp[max_n][3];//dp(u,0)表示该节点和父节点不是服务器,dp(u,1)表示该节点是服务器,dp(u,2)表示该节点不是服务器但父节点是服务器
int n;
vector<int> son[max_n];
bool vis[max_n];
void dp_(int root)
{
vis[root]=1;
dp[root][0]=max_n;
dp[root][1]=1;
dp[root][2]=0;
queue<int> q;
for(int i=0;i<son[root].size();i++)
if(!vis[son[root][i]])
{
vis[son[root][i]]=1;
q.push(son[root][i]);
dp_(son[root][i]);
vis[son[root][i]]=0;
dp[root][2]+=dp[son[root][i]][0];
dp[root][1]+=min(dp[son[root][i]][2],dp[son[root][i]][1]);
}
for(int i=0;i<son[root].size();i++)
if(!vis[son[root][i]])
dp[root][0]=min(dp[root][0],dp[root][2]+dp[son[root][i]][1]-dp[son[root][i]][0]);
}
void solve(void)
{
memset(vis,false,sizeof(vis));
dp_(1);
printf("%d\n",min(dp[1][0],dp[1][1]));
}
int main(void)
{
// freopen("out.txt","w",stdout);
while(true)
{
scanf("%d",&n);
for(int i=1;i<=n-1;i++)
{
int a,b;
scanf("%d%d",&a,&b);
son[a].push_back(b);
son[b].push_back(a);
}
int t;
scanf("%d",&t);
solve();
if(t==-1)break;
for(int i=1;i<=n;i++)
son[i].clear();
}
// fclose(stdout);
}