【树形DP】树

树(tree)


【问题描述】
图论中的树为一个无环的无向图。给定一棵树,每个节点有一盏指示灯和一个按钮。如果节点的按扭被按了,那么该节点的灯会从熄灭变为点亮(当按之前是熄灭的),或者从点亮到熄灭(当按之前是点亮的)。并且该节点的直接邻居也发生同样的变化。 开始的时候,所有的指示灯都是熄灭的。请编程计算最少要按多少次按钮,才能让所有节点的指示灯变为点亮状态。
【输入格式】
输入文件有多组数据。
输入第一行包含一个整数n,表示树的节点数目。每个节点的编号从1到n。
输入接下来的n – 1行,每一行包含两个整数x,y,表示节点x和y之间有一条无向边。
当输入n为0时,表示输入结束。
【输出格式】
对于每组数据,输出最少要按多少次按钮,才能让所有节点的指示灯变为点亮状态。每一组数据独占一行。

输入样例:
3
1 2
1 3
0
输出样例:
1

这道题目莫名让我想到了以前见过有一个死胖子用手指按字母然后附近的都会点到,不过我忘记题目了~~
言归正传,题目已经很明确告诉你是一棵树,那么就可以很容易想到是树形DP,寻找状态转移方程是本题的关键。
首先,我们用f[x][0]表示以x为根节点的子树下,x开着灯,而他的儿子,孙子,孙子的儿子...也都开灯的最少次数;

f[x][1]表示以x为根节点的子树下,x关灯,而他的子辈都开着灯的最少次数;

f[x][2]则表示以x为根节点的子树下,x关灯,他的子辈也都关灯的最少次数;

那么f[x][3]表示什么呢,就是以x为根节点的子树下,x开灯,他的子辈也都关灯的最少次数,很显然,这是不存在的,所以f[x][3]排除啦。

把F数组的定义弄清楚了,那么接下来的就好办了!
也是分三步:

  1. f[x][0]最好算,因为如果我和我的子辈都开着灯了,那么我的亲朋好友就不用开灯了啦,所以f[x][0]+=f[x][2]

2.接着就算f[x][2]f[x][1]怎么算呢?因为我们还要考虑到有可能在我的子辈按按钮按了个奇数,那么最后的状态就要改变呀(ps:真是个无聊的儿子。。),所以我需要一个变量来算我途中按了多少次(这个变量我称为按钮计数)。然后就是如何“科学的推卸责任了”,我们也要用一个变量,来表示最少次数,那就是tmp+=min(f[x][2],f[x][1])啦!

3.f[x][2]和f[x][1]怎么继承呢?那就是青——藏——高——原,啊,说偏了,这里就体现了我们那个最最最最最可爱的按钮计数了(2点说过,不知道给我重看!),这里我可以告诉你一个技巧,奇数和偶数是刚好相反的,因为状态是刚好相反的嘛,所以你想好奇数以后就直接把奇数调换顺序就行了,奇数的话那f[x][2]=t,而偶数的话就是f[x][1]=t,剩下的呢,我先不告诉你,你自己先想,如果想不出就直接看代码啦!

那么我们就找到了状态转移方程,接着就是制作编目录啦,balabala的,整道题就完成啦!
下面我附上代码,代码中有详细解释:

#include<cmath>
#include<cstdio>
#include<cstring>
#define min(a,b) ((a)<(b)?(a):(b))
using namespace std;
typedef long long ll;//据说这个比long long更大,不过后面输出要用llu。
//你细心可以发现我跟本没用long long^_^ 
struct node//边目录 
{
    int x,y,next;//x和y表示一条边的两个端点,next表示下一条边的编号 
}a[210];int len,last[210];//last表示与当前边相连的最后一条边的编号,len是边的数目 
void ins(int x,int y)
{
    len++;//边的数目增加一条 
    a[len].x=x;a[len].y=y;//给边赋值 
    a[len].next=last[x];last[x]=len;//边的联系 
}
int f[210][3];//我们上面讲到的定义 
bool b[210];//因为我用的是双向边,而有可能会进入死循环 
void treeDP(int x)
{
    /*
    f[x][0]x开灯,开灯 
    f[x][1]x不开灯,开灯 
    f[x][2]x不开灯,不开灯 
    */
    f[x][0]=1;f[x][1]=f[x][2]=0;//一开始的以x为结点的子树(包括点x)全部开灯就有一种方法:点亮x
    //其他的归零 
    int minn=99999999,num=0,tmp=0;//这里的minn用的十分巧妙,num就是按钮计数,tmp就是存储最少次数 
    for(int k=last[x];k;k=a[k].next)//访问x的亲朋好友 
    {
        int y=a[k].y;//找到x点所在的边的另一个点y 
        if(b[y]==false)//判断是否找过y 
        {
            b[y]=true;treeDP(y);//标记找过,然后递归找y 
            f[x][0]+=f[y][2];//如果我和我的子辈都开着灯了,那么我的亲朋好友就不用开灯了啦! 
             
            tmp+=min(f[y][0],f[y][1]);//去最小的值 
            minn=min(abs(f[y][0]-f[y][1]),minn);//这里非常巧妙:
            /*假设f[y][0]>f[y][1]的话,那么此时tmp加的就是f[y][1],而minn里面的就是f[y][0]-f[y][1]
            那么tmp+minn实际上就是f[y][1]+f[y][0]-f[y][1]=f[y][0]
            而反之,tmp加的就是f[y][0],那么minn里面的就是f[y][1]-f[y][0]
            那么tmp+minn就是f[y][0]+f[y][1]-f[y][0]=f[y][1]了
            */ 
            if(f[y][1]>=f[y][0])num++;
            //如果不开灯比开灯要贵,那么我们肯定选开灯的,而此时就按了一次按钮,所以num++ 
        }
    }
    if(num%2==1)
    {
        f[x][2]=tmp+minn;//此时,x不开灯,而他的子辈也不开灯,就要让其中一个儿子帮他开灯 
        f[x][1]=tmp;//此时,x不开灯,要他的子辈开灯,那怎么无缘无故开灯呢?所以就让他的儿子去开灯 
    }
    else
    {
        f[x][2]=tmp;//同上,我说过是相反的 
        f[x][1]=tmp+minn;
    }
}
int main()
{
    int n;
    while(scanf("%d",&n)!=EOF)
    {
        memset(f,0,sizeof(f));
        if(n==0)break;
        len=0;memset(last,0,sizeof(last));
        for(int i=1;i<n;i++)
        {
            int x,y;scanf("%d%d",&x,&y);
            ins(x,y);ins(y,x);
        }
        memset(b,false,sizeof(b));b[1]=true;
        treeDP(1);//从1开始递归,因为1是根节点 
        printf("%d\n",min(f[1][0],f[1][1]));//最后找这两个的最小值就行了! 
    }
    return 0;
}

谢谢你的观看,举一反三,你可以上caioj.cn去做树形动态规划的8道题目哦!

猜你喜欢

转载自www.cnblogs.com/candy067/p/11401969.html