【51nod1531】树上的博弈

在一颗有n个节点的有根树中,两玩家进行棋子移动博弈,目标是使棋子落在数字最大的或最小的叶子节点。通过树形DP解决此问题,先手希望最大化结果,后手则最小化。分析表明,先手会选择子树最小的路径,而后手会选择子树数量最多的路径。最终状态转移方程考虑子树中权值大于最终结果的叶子数量。

题目描述

有一棵n个点的有根树,他有m个叶子结点(叶子结点是那些没有孩子的结点)。边由父亲指向孩子。数字1到m被分配到每一个叶子中。每一个叶子有一个数字,并且每一个数字恰好被分配到一个叶子中。

刚开始的时候根部有一个棋子。两个玩家轮流移动棋子,每一步都会将这个棋子向他的某一个孩子移动;如果玩家不能再移动棋子了,那么游戏结束。游戏的结果就是棋子所在叶子上面的数字。游戏的先手想要这个数字最大化,而后手想要这个数字最小化。

山巴布里想要给这些叶子分配数字使得最终结果最大,而马族塔想要给这些叶子分配数字使得最终结果最小。那么当山巴布里来分配数字的时候游戏结果会是多少?马族塔分配的时候又是多少呢?山马布里和马族塔并不参加游戏,而是另外两个非常聪明的人来参加游戏。

样例解释:在这个样例中,树有三个叶子:3,4和5。如果把数字3分配给3号结点,那么第一个选手就能够让棋子到达那儿,最终结果就是3。另一方面,很明显,无论怎么分配数字,第一个选手让棋子达到最小数字是2。


Input
单组测试数据。
第一行有一个整数n (1≤n≤2*10^5),表示树中结点的数目。
接下来n-1行,每一行两个整数ui 和 vi (1≤ui,vi≤n) ,表示树中的边,由ui指向vi。
输入保证是一棵有根树,而且根是1。
Output
输出两个整数表示最大值和最小值,以空格分开。
Input示例
样例输入1
5
1 2
1 3
2 4
2 5
Output示例
样例输出1
3 2

题解

树形dp好题。

 经过思考了好几种状态之后终于把这题想出来了。

最大值和最小值是类似的,所以我们只考虑最大值就行了,我们考虑,对于先手来说,他一定要走子树最小的那一颗子树,而对于后手来说他一定走子树最多的那颗,但后手不是取max而是相加,因为它有走所有的可能啊。

所以定义f[i]表示在子树i中最终有多少个叶子权值比最后结果大,把树的深度按奇偶讨论就行了。

/*
 树形dp 
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=200010;
const int inf=1e9;
int pre[N*2],last[N],other[N*2],num;
int f[N],dep[N],n,chu[N];
inline void add(int x,int y){
	num++;
	pre[num]=last[x];
	last[x]=num;
	other[num]=y;
}
void work(int x,int fa){
	if(chu[x]==0) f[x]=1;
	for(int i=last[x];i;i=pre[i]){
		int v=other[i];
		if(v!=fa){
			dep[v]=dep[x]+1;
			work(v,x);
		}
	}
	if(dep[x]&1){
		if(chu[x]!=0) f[x]=inf; 
		for(int i=last[x];i;i=pre[i]){
			int v=other[i];
			if(v!=fa)
			f[x]=min(f[x],f[v]);
		}
	}
	else{
		for(int i=last[x];i;i=pre[i]){
			int v=other[i];
			if(v!=fa)
			f[x]+=f[v]; 
		}
	}
}
void work1(int x,int fa){
	if(chu[x]==0) f[x]=1;
	for(int i=last[x];i;i=pre[i]){
		int v=other[i];
		if(v!=fa){
			dep[v]=dep[x]+1;
			work1(v,x);
		}
	}
	if(dep[x]&1){
		for(int i=last[x];i;i=pre[i]){
			int v=other[i];
			if(v!=fa)
			f[x]+=f[v];
		}
	}
	else{
		if(chu[x]!=0) f[x]=inf;
		for(int i=last[x];i;i=pre[i]){
			int v=other[i];
			if(v!=fa)
			f[x]=min(f[x],f[v]);
		}
	}
}
int main(){
	int x,y;
	scanf("%d",&n);
	for(int i=1;i<n;i++) {
		scanf("%d%d",&x,&y);
		add(x,y);add(y,x);
		chu[x]++;
	}
	int ans=0;
	for(int i=1;i<=n;i++) if(chu[i]==0) ans++;
	
	dep[1]=1;
	work(1,1);
	printf("%d ",ans-f[1]+1);
	
	dep[1]=1;
	memset(f,0,sizeof(f));
	work1(1,1);
	printf("%d\n",f[1]);
	return 0;
}

### 关于51Nod 3100 上台阶问题的C++解法 #### 题目解析 该题目通常涉及斐波那契数列的应用。假设每次可以走一步或者两步,那么到达第 \( n \) 层台阶的方法总数等于到达第 \( n-1 \) 层和第 \( n-2 \) 层方法数之和。 此逻辑可以通过动态规划来解决,并且为了防止数值过大,需要对结果取模操作(如 \( \% 100003 \)[^1])。以下是基于上述思路的一个高效实现: ```cpp #include <iostream> using namespace std; const int MOD = 100003; long long f[100010]; int main() { int n; cin >> n; // 初始化前两项 f[0] = 1; // 到达第0层有1种方式(不移动) f[1] = 1; // 到达第1层只有1种方式 // 动态规划计算f[i] for (int i = 2; i <= n; ++i) { f[i] = (f[i - 1] + f[i - 2]) % MOD; } cout << f[n] << endl; return 0; } ``` 以上代码通过数组 `f` 存储每层台阶的结果,利用循环逐步填充至目标层数 \( n \),并最终输出结果。 --- #### 时间复杂度分析 由于仅需一次线性遍历即可完成所有状态转移,时间复杂度为 \( O(n) \)。空间复杂度同样为 \( O(n) \),但如果优化存储,则可进一步降低到 \( O(1) \): ```cpp #include <iostream> using namespace std; const int MOD = 100003; int main() { int n; cin >> n; long long prev2 = 1, prev1 = 1, current; if (n == 0 || n == 1) { cout << 1 << endl; return 0; } for (int i = 2; i <= n; ++i) { current = (prev1 + prev2) % MOD; prev2 = prev1; prev1 = current; } cout << prev1 << endl; return 0; } ``` 在此版本中,只保留最近两个状态变量 (`prev1`, `prev2`) 来更新当前值,从而节省内存开销。 --- #### 输入输出说明 输入部分接受单个整数 \( n \),表示台阶数量;程序会返回从地面走到第 \( n \) 层的不同路径数目,结果经过指定模运算处理以适应大范围数据需求。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值