题目描述
某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。
输入
输入文件的第一行包含两个用空格隔开的整数N和M,其中2≤N≤3000,1≤M≤N-1,N为整个有线电视网的结点总数,M为用户终端的数量。
第一个转播站即树的根结点编号为1,其他的转播站编号为2到N-M,用户终端编号为N-M+1到N。
接下来的N-M行每行表示—个转播站的数据,第i+1行表示第i个转播站的数据,其格式如下:
K A1 C1 A2 C2 … Ak Ck
K表示该转播站下接K个结点(转播站或用户),每个结点对应一对整数A与C,A表示结点编号,C表示从当前转播站传输信号到结点A的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。
输出
输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。样例输入
5 3 2 2 2 5 3 2 3 2 4 3 3 4 2
样例输出
2
提示
如下图所示,共有五个结点。结点①为根结点,即现场直播站,②为一个中转站,③④⑤为用户端,共M个,编号从N-M+1到N,他们为观看比赛分别准备的钱数为3、4、2,从结点①可以传送信号到结点②,费用为2,也可以传送信号到结点⑤,费用为3(第二行数据所示),从结点②可以传输信号到结点③,费用为2。也可传输信号到结点④,费用为3(第三行数据所示),如果要让所有用户(③④⑤)都能看上比赛,则信号传输的总费用为:
2+3+2+3=10,大于用户愿意支付的总费用3+4+2=9,有线电视网就亏本了,而只让③④两个用户看比赛就不亏本了。
我用的是多叉树转二叉树的方法,这题只要分四种情况考虑就行了:
① 这是用户节点,且没有兄弟(右儿子);
② 这是一个用户节点,但有右子(兄弟节点);
③ 这是转播站,且没有兄弟节点;
④ 这是个转播站,且有兄弟节点。
详细过程可以看代码,里面有很详细的注释:
#include <cstdio> struct tree { int lc,rc; //多叉树转二叉树:左儿子右兄弟 int val; //选择选择节点i时能赚多少钱(该节点的值(如果是中转站则为0)-父节点连到这个节点的边的值) int num; //表示该节点为根的树,最多有多少个用户节点 }; const int maxn=3000; int n,m; tree a[maxn+1]; int dp[maxn+1][maxn+1]; int maximum(int x, int y) { if (x>y) return x; return y; } //计算以x为根的树最多有多少个用户节点 int count(int x) { int res=0; //如果自己是用户节点,答案+1 if (a[x].lc==0) res++; //如果有左右子,则递归统计 if (a[x].rc!=0) res+=count(a[x].rc); if (a[x].lc!=0) res+=count(a[x].lc); a[x].num=res; return res; } void init() { scanf("%d%d",&n,&m); //第i个转播站的信息 for (int i=1; i<=n-m; i++) { int ii,k,vv; scanf("%d",&k); //对k=1的特殊处理,要放在这个转播站的左儿子上 scanf("%d%d",&ii,&vv); a[i].lc=ii; a[ii].val-=vv; //其余的放在之前处理过的兄弟节点的右儿子上 for (int j=2; j<=k; j++) { int x,v; scanf("%d%d",&x,&v); a[ii].rc=x; a[x].val-=v; ii=x; } } //用户节点的数据 for (int i=n-m+1; i<=n; i++) { int x; scanf("%d",&x); a[i].val+=x; } //计算结构体中的num的值,搜索时有用 count(1); } //dp[root][x]表示以root为根的树,里面有x个用户节点,所能赚到的最多的钱数 int dfs(int root, int x) { if (dp[root][x]!=0) return dp[root][x]; //记忆化搜索 if (x==0) return 0; //如果没有连任何一个用户节点,则不赚不亏,返回0 //如果这是用户节点,且没有兄弟(右儿子),那么就加上这个节点的值 if (a[root].lc==0&&a[root].rc==0) return dp[root][x]=a[root].val; //如果这是转播站且没有兄弟节点,那么只能往左子(儿子节点)走 //因为经过这个节点,所以要加上这个答案。再递归左子树。 if (a[root].lc!=0&&a[root].rc==0) return dp[root][x]=dfs(a[root].lc,x)+a[root].val; //如果是一个用户节点,但有右子(兄弟节点) //那么分选与不选两种情况考虑 if (a[root].lc==0&&a[root].rc!=0) { //选的话要加上它的值 int res=dfs(a[root].rc,x-1)+a[root].val; //不选的话有限制条件:现在要保留的用户节点数(x)不大于它右子树里的用户节点数 //即保证它的右子里至少有x个用户节点 if (a[a[root].rc].num>=x) res=maximum(res,dfs(a[root].rc,x)); //取两种情况中的较优情况 return dp[root][x]=res; } //否则这是个转播站,且有兄弟节点 int res=-0x7F7F7F7F; //枚举在分给左子树i个用户节点,右子树x-i个用户节点的可能性,取最优 //特别注意:当不传给这个中转站,只给它的兄弟节点(右子)时,可以不取这个节点的值! //故i从1开始,而非0(我因为这个WA了好多次) for (int i=1; i<=x; i++) { if (a[a[root].lc].num<i) continue; //如果左子树里只有不到i个用户节点,不满足条件 if (a[a[root].rc].num<x-i) continue; //右子同理 res=maximum(res,dfs(a[root].lc,i)+dfs(a[root].rc,x-i)+a[root].val); } //如果全给其兄弟节点时兄弟节点里可以有这么多用户节点(num>=x),则取较优值 if (a[a[root].rc].num>=x) res=maximum(res,dfs(a[root].rc,x)); return dp[root][x]=res; } int main() { init(); for (int i=m; i>=0; i--) { //从m~0逆序枚举给i个用户信号的情况 int ans=dfs(1,i); //从根节点(1)开始记忆化搜索 //如果不会亏本,则跳出循环,直接输出答案 if (ans>=0) { printf("%d\n",i); break; } } return 0; }