UVA 10859 Placing Lampposts
题目大意:
给一个n点m边的无向无环图,在尽量少的节点上放灯,使得所有边都被照亮,一个点上的灯可以照亮与该点相连的所有边,在灯最少的前提下,使得被两盏灯照亮的边尽可能多.输出所需灯个数,被两盏灯照亮的边个数,被一盏灯照亮的边个数.
题目分析:
无向无环图,显然就是一棵无根树.而m有可能小于n-1,那么就可能是多棵树,即森林.
显然这是树形DP,但是需要维护的值有两个最少灯个数和最多边个数.其实边只有两种:被一盏灯照亮和被两盏灯照亮.所以要使被两盏灯照亮的边尽可能多,就是要使被一盏灯照亮的边尽可能少.就变成了求两个最小值.
对于两个最小值的求解,白书上面给出了一种巧妙的办法,把两者和在一起.
x=Ma+b
其中,M是一个很大的正整数,M应当大于b的理论最大值与a的理论最小值之差.因为在a不同的情况下,b是不能影响dp值结果的,所以a的单位1M需要大于b的最大值.这样的好处在于如此比较dp值先比较a再比较b,比较简便(但是需要注意M不要取太大,避免溢出).
既然是DP,首先考虑其状态的定义,d(u,p,k)表示以u结点为根的子树全部照亮所需的最小dp值,其中p表示u的父节点,k表示父节点的状态:是否有灯.
代码:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int M=2000;//分割两个量的常量
const int maxn=1000+10;
int fir[maxn],nxt[maxn<<1],to[maxn<<1],ecnt;
void add_edge(int u,int v)
{
nxt[++ecnt]=fir[u];fir[u]=ecnt;to[ecnt]=v;
nxt[++ecnt]=fir[v];fir[v]=ecnt;to[ecnt]=u;
}
int d[maxn][2],vis[maxn][2];
int dp(int u,int p,int k)//dp值由两部分组成,dp/M表示灯的个数,dp%M表示仅有一盏灯照亮的道路数
//dp(u,p,k)表示当前以u节点为根的子树所需要的最小dp值,其中p表示u父节点,p=-1时,u为根,k表示父节点有没有灯 0无1有
{
if(vis[u][k]) return d[u][k];//记忆化
vis[u][k]=1;
int& ans=d[u][k];ans=M;//无论p是否有灯,u均可以有灯
for(int i=fir[u];i;i=nxt[i]) if(to[i]!=p) ans+=dp(to[i],u,1);
if(p!=-1&&!k) ++ans;//若该点不是根并且p没灯,那么p与u之间的道路仅有一盏灯照亮
if(p==-1||k) {//若u为根或者p有灯,那么u可以没灯
int tot=0;
for(int i=fir[u];i;i=nxt[i]) if(to[i]!=p) tot+=dp(to[i],u,0);
if(p!=-1) ++tot;//若u不为根,那么p与u之间的道路仅有一盏灯照亮
ans=min(ans,tot);
}
return ans;
}
void init()//初始化
{
ecnt=0;
memset(fir,0,sizeof(fir));
memset(vis,0,sizeof(vis));
}
int main()
{
int T;
scanf("%d",&T);
while(T--) {
init();//记得每次初始化
int n,m;
scanf("%d%d",&n,&m);
for(int u,v,i=0;i<m;i++) {
scanf("%d%d",&u,&v);
add_edge(u,v);
}
int ans=0;
for(int i=0;i<n;i++) if(!vis[i][0]) ans+=dp(i,-1,0);//一旦存在未访问过的点,做一次树形dp
printf("%d %d %d\n",ans/M,m-ans%M,ans%M);
}
return 0;
}