算法刷题记录(Day 53)

Apple Tree(poj 2486)

原题链接
题目类型:树形dp

思考过程:K是大于N的,这也就意味着一次的移动不仅仅只是邻接的。
->处理出任意两点之间的距离->lca问题 ->但是既然移动过来了,就一定会吃->K比N大是用于一棵子树的多条吃的路径的
有点类似于0、1背包的问题,value是苹果的个数,weight是两个节点之间的距离,若仅仅使用二维的dp来进行记录,dp[i][j]代表最后一步到节点i时走了j步的最大值,->这样会限制走的节点的标号是递增的。但是如果不将dp[i][j]的最后一步限制在i上,那么将无法进行递推下去。->可以增加一个数组来记录上一回的最后一个子树的长度吗?

树形递推呢?
dp[u][j]代表的是以u为根的树,限制步数为j时,最多能吃到的 苹果树。假设其子节点分别为v1,v2,v3…
对于v1,dp[u][j]初始化为dp[u][j]=dp[v1][j-1]+num[u]
对于v2,dp[u][j]=max(dp[u][i],dp[v2][p-2*k-1 ]+dp[u][k])(k=0,1,2,3…p-1)->注意,这里的递推如果简单进行乘2会出现问题,因为回撤的时候仅仅只是在最后的一棵子树上,简单的乘2会导致第一棵子树一直在来回走

超过了节点数怎么办?->把它全部设置为目前的最大的苹果数

//WA
#include<iostream>
using namespace std;
#define NMAX 105
#define KMAX 210
int num[NMAX];
int is[NMAX];
int N, K;
int dp[NMAX][KMAX];
struct Edge {
	int from, to, nxt;
}E[2 * NMAX];
int head[NMAX], cnt = 1;
void add(int u, int v) {
	E[cnt].from = u, E[cnt].to = v, E[cnt].nxt = head[u];
	head[u] = cnt++;
	E[cnt].from = v, E[cnt].to = u, E[cnt].nxt = head[v];
	head[v] = cnt++;
}
void dfs(int u) {
	int sub[KMAX];
	is[u] = 1;
	dp[u][0] = num[u];
	sub[0] = 0;
	int first = 1;
	for (int i = head[u]; i; i = E[i].nxt) {
		int v = E[i].to;
		if (!is[v]) {
			dfs(v);
			/*if (u == 1) {
				cout << "test" << endl;
			}*/
			for (int j = K; j > 0; j--) {
				if (first) {
					dp[u][j] = dp[v][j - 1] + num[u];
					sub[j] = j;

				}
				else {
					for (int k = 0; j - k - sub[k] - 1 >= 0; k++) {
						//dp[u][j] = max(dp[u][j], dp[u][k] + dp[v][j - 2 * k - 1]);
						if (dp[u][j] < dp[u][k] + dp[v][j - k - sub[k] - 1]) {
							dp[u][j] = dp[u][k] + dp[v][j - k - sub[k] - 1];
							sub[j] = j - k - sub[k];
						}
					}
				}
			}
			first = 0;
		}
	}
	if (first) {
		//填充完叶子节点
		for (int i = 1; i <= K; i++) dp[u][i] = num[u];
	}
}
int main() {
	while (scanf_s("%d %d", &N, &K) == 2) {
		memset(is, 0, sizeof(is));
		memset(head, 0, sizeof(head));
		memset(dp, 0, sizeof(dp));
		cnt = 1;
		for (int i = 1; i <= N; i++) {
			scanf_s("%d", &num[i]);
		}
		for (int i = 0; i < N - 1; i++) {
			int u, v;
			scanf_s("%d %d", &u, &v);
			add(u, v);
		}
		dfs(1);
		printf("%d\n", dp[1][K]);
	}
}

WA原因分析:
在这里插入图片描述
如上图所示的情况下,仅仅改变了节点2和3的输入顺序,却导致了结果的不同,这是因为,如果先输入3再输入2,根据上述的子树节点的递推关系,就没有先经过3再经过2的情况,即和正确的结果失之交臂。

思路的改进:
参考思路
思路的修正:
1.树形dp在一层上仍然可以再使用一种dp方法,对于这种dp的顺序有较大影响的(如在WA中分析的那样),需要去使用不同的数据结构来进行表示不同的状态。
2.dp[i][j][0]代表的是以i为根节点,最多走j步且必须回到i的最大苹果数
dp[i][j][1]代表的是以i为根节点,最多走j步且不必回到i的最大苹果数
首先全部初始华为0,然后将dp[i][j][0]和dp[i][j][1]初始化为节点i的苹果数

//AC
#include<iostream>
using namespace std;
#define NMAX 105
#define KMAX 210
int num[NMAX];
int is[NMAX];
int N, K;
int dp[NMAX][KMAX][2];
struct Edge {
	int from, to, nxt;
}E[2 * NMAX];
int head[NMAX], cnt = 1;
void add(int u, int v) {
	E[cnt].from = u, E[cnt].to = v, E[cnt].nxt = head[u];
	head[u] = cnt++;
	E[cnt].from = v, E[cnt].to = u, E[cnt].nxt = head[v];
	head[v] = cnt++;
}
void dfs(int u) {
	is[u] = 1;
	for (int i = 0; i <= K; i++) dp[u][i][0] = dp[u][i][1] = num[u];
	for (int i = head[u]; i; i = E[i].nxt) {
		int v = E[i].to;
		if (!is[v]) {
			dfs(v);
			for (int j = K; j >= 0; j--) {
				for (int k = 0; k <= K; k++) {
					if (j - k - 2 >= 0) dp[u][j][0] = max(dp[u][j][0], dp[u][k][0] + dp[v][j - k - 2][0]);
					if (j - k - 2 >= 0) dp[u][j][1] = max(dp[u][j][1], dp[u][k][1] + dp[v][j - k - 2][0]);
					if (j - k - 1 >= 0) dp[u][j][1] = max(dp[u][j][1], dp[u][k][0] + dp[v][j - k - 1][1]);
				}
			}
		}
	}

}
int main() {
	while (scanf_s("%d %d", &N, &K) == 2) {
		memset(is, 0, sizeof(is));
		memset(head, 0, sizeof(head));
		memset(dp, 0, sizeof(dp));
		cnt = 1;
		for (int i = 1; i <= N; i++) {
			scanf_s("%d", &num[i]);
		}
		for (int i = 0; i < N - 1; i++) {
			int u, v;
			scanf_s("%d %d", &u, &v);
			add(u, v);
		}
		dfs(1);
		printf("%d\n", dp[1][K][1]);
	}
}

总结

1.树形dp问题,是含有两个dp过程的,一个是基于树的结构,另一个是在每一层之间
2.dp问题可以适当增加状态的表示,从而正确解题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值