树形动态规划练习《蓝桥杯 结点选择》
问题描述
有一棵 n 个节点的树,树上每个节点都有一个正整数权值。如果一个点被选择了,那么在树上和它相邻的点都不能被选择。求选出的点的权值和最大是多少?
输入格式
第一行包含一个整数 n 。
接下来的一行包含 n 个正整数,第 i 个正整数代表点 i 的权值。
接下来一共 n-1 行,每行描述树上的一条边。
输出格式
输出一个整数,代表选出的点的权值和的最大值。
样例输入
5
1 2 3 4 5
1 2
1 3
2 4
2 5
样例输出
12
样例说明
选择3、4、5号点,权值和为 3+4+5 = 12 。
数据规模与约定
对于20%的数据, n <= 20。
对于50%的数据, n <= 1000。
对于100%的数据, n <= 100000。
权值均为不超过1000的正整数。
解题过程
刚学习完树形动态规划的原理,所以乍一看就知道此题应该用树形动态规划解决。分两步:1、建树。2、动态规划。
刚开始选择的存储结构是二维数组,既每一行表示树的一层,每一列表示该层(行)的所有节点;记录下树的最大层数,从最后一层开始改变每个节点的状态,最后从根节点中获取最优解。
#include<stdio.h> #include<stdlib.h> #include<string.h> #define M 100010 //数组最大长度 int fu[M],hz[M][M],shu[M][M],pow[M],f[M][2]; //父节点数组; 孩子数组hz[i][0]第i个节点的孩子数,hz[i][j](j>0)表示i节点的第j个孩子 //树二维数组,shu[i][0]表示第i层节点数,shu[i][j](j>0)表示第i层的第j个节点; //pow[]权值数组,p[i]表示第i个节点的权值 //f[i][1]保留节点i时最大权值,f[i][0]不保留节点i时的最大权值 int main() { int n,i,j,u,v; memset(fu,0,sizeof(fu)); memset(hz,0,sizeof(hz)); memset(shu,0,sizeof(shu)); memset(f,0,sizeof(f)); scanf("%d",&n); for(i=1;i<=n;i++)scanf("%d",&pow[i]); for(i=1;i<n;i++) { scanf("%d%d",&u,&v); fu[v]=u; hz[u][0]++; hz[u][hz[u][0]]=v; } //建树 int x,maxlev=-1,s; for(i=1;i<=n;i++) { x=fu[i]; s=1; while(x!=0){s++;x=fu[x];} shu[s][0]++; shu[s][shu[s][0]]=i; if(s>maxlev)maxlev=s; } //动态规划 int now,k,a,b; for(i=maxlev;i>0;i--) { for(j=1;j<=shu[i][0];j++) { now=shu[i][j]; if(hz[now][0]==0) { f[now][0]=0; f[now][1]=pow[now]; } else { for(k=1;k<=hz[now][0];k++) { a=f[hz[now][k]][0]; b=f[hz[now][k]][1]; f[now][1]+=a; if(b>a)a=b; f[now][0]+=a; } } } } int sum=0; for(i=1;i<=shu[1][0];i++) { now=shu[1][i]; a=f[now][0];b=f[now][1]; if(b>a)a=b; sum+=a; } printf("%d\n",sum); return 0; }
按理说这个算法是可行的,但是再提交答案时,居然发生运行错误,我看了看内存使用率非常大,返回题目看了数据规模,节点数n<=100000,也就意味着要用二维数组存储树的话,二维数组至少定义为shu[100000][100000],占用了非常大的控件资源。再者,题目给n个顶点,n-1条边,也就意味着树没有孤立点,并且有且仅有一个根节点,可见每一层的节点很多时候是远少于100000的,所以应该改用动态存储结构。
树的存储结构
《1》、双亲表示法
假设以一组连续空间存储树的节点,同时在每个节点中附设一个指示器指示其双亲节点在链表中的位置,其形式说明如下:
#define MAX_TREE_SIZE 100 typedef struct PTNode{//节点结构 TElemType data; int parent;//双亲位置 }PTNode; typedef struct{ //树结构 PTNode nodes[MAX_TREE_SIZE]; int r,n; //根节点位置和节点数 }PTree;
这种存储结构利用了每个节点(除根节点以外)只有唯一双亲的性质。PARENT(T,x)操作可以在常数时间内实现。反复调用PARENT操作,直到遇见无双亲的节点时,便找到了树的根,这个就是ROOT(x)的过程。但是,在这种表示法中,求节点的孩子时需要遍历整个结构。
《2》、孩子表示法
这里主要给出一种类似于邻接表的表示法。把每个节点的孩子节点排列起来,看成是一个线性表,且以单链表作为存储结构,则n个节点有n个孩子链表(叶子节点的孩子链表为空表)。而n个头指针又组成一个线性表,为了便于查找,可采用顺序存储结构。这种存储结构可形式地说明如下:
#define MAX_TREE_SIZE 100 typedef struct CTNode{ //孩子节点 int child; struct CTNode *next; }*ChildPtr; typedef struct{ TElemType data; ChildPtr firstchild; //孩子链表头指针 }CTBox; typedef struct{ CTBox nodes[ MAX_TREE_SIZE ]; int n,r; //节点数和根节点位置 }CTree;
与双亲表示法相反,孩子表示法便于那些涉及孩子操作的实现,却不适合用于PARENT(T,x)的操作。我们可以把双亲表示法和孩子表示法合起来,既将双亲表示和孩子链表和在一起。
《3》、孩子兄弟表示法
又称二叉树表示法,或二叉树表示法。既以二叉树表作树的存储结构。链表中节点的两个链域分别指向该节点的第一个孩子节点和下一个兄弟节点,分别命名为firstchild域和nextsibling域。存储结构形式说明如下:
#define MAX_TREE_SIZE 100 typedef struct CSNode{ ElemType data; struct CSNode *firstchild,*nextsibling; }CSNode,*CSTree;
利用这种结构便于实现各种树的操作。
符合题目要求的结果
采用了树存储结构中的《孩子表示法》,当然有些改进。
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<algorithm> #define M 100100 //最大长度 using namespace std; //孩子节点结构 typedef struct Node { int vex; Node* next; }Child; Child* head[M];//链表头数组 int f[M][2],pow[M],visit[M]; //pow[]权值数组,p[i]表示第i个节点的权值 //f[i][1]保留节点i时最大权值,f[i][0]不保留节点i时的最大权值 //visit[i]==1表示i点被访问过,visit[i]==0表示节点i未被访问过 //添加边(对称的) void addADJ(int u,int v) { Child *p,*q; p=(Child*)malloc(sizeof(Child)); p->vex=v; p->next=head[u]; head[u]=p; q=(Child*)malloc(sizeof(Child)); q->vex=u; q->next=head[v]; head[v]=q; } //动态规划获取结果 void GetResul(int v) { visit[v]=1; Child *p; for(p=head[v];p!=NULL;p=p->next) { if(visit[p->vex]==0) { GetResul(p->vex); f[v][1] = f[v][1]+f[p->vex][0]; f[v][0]+=max(f[p->vex][0],f[p->vex][1]); } } f[v][1]+=pow[v]; } int main() { int i,j,u,v,n; memset(head,NULL,sizeof(head)); memset(f,0,sizeof(f)); memset(visit,0,sizeof(visit)); scanf("%d",&n); for(i=1;i<=n;i++) { scanf("%d",&pow[i]); } for(i=1;i<n;i++) { scanf("%d%d",&u,&v); addADJ(u,v); } GetResul(1);//从节点1开始进行动态规划 printf("%d\n",max(f[1][0],f[1][1]));//结果输出 return 0; }