CSP——博弈论与石子合并

题目背景

小 c 和 小 z 学习了博弈论的落后知识,他们打算玩 Nim 游戏以提高博弈论的先进性。

问题描述

众所周知,Nim 游戏本质上就是幼儿园小朋友的玩石子的小游戏,于是他们找到了许多石子,并将其分为 n 堆并排成一排,从左到右第 i 堆有 ai 个石子。

而说到石子,当然就不能不提到大名鼎鼎的动态规划入门经典题目《石子合并》。

所以在最初的规则中由两人轮流进行操作,每次操作者可以选择合并两堆相邻的石子,如此操作直到剩下一堆石子。小 c 希望这堆石子尽量少,小 z 则希望这堆石子尽量多。

小 c 和小 z 希望知道在二人绝顶聪明的情况下最终这堆石子的大小是多少——才怪,显然按照上述规则最终一堆石子的大小一定是 ∑i=1nai,与二人的操作无关。

于是他们决定增添一些新的规则,新规则如下:仍由两人轮流操作,每次操作者可以选择合并两堆相邻的石子,或者扔掉目前最靠左的一堆石子,或者扔掉目前最靠右的一堆石子(不能不操作),直到剩下一堆石子。小 c 希望这堆石子尽量少,小 z 则希望这堆石子尽量多。

仍然假设二人聪明绝顶(虽然这在实际上并不可能(至少对小 c 不可能)),问最后这堆石子的大小是多少?

注意,为了公平起见,每次游戏开始时他们会决定谁先手,而不是固定的由小 c 先手或者小 z 先手。

输入格式

第一行两个整数 n,k;

​ 其中 k∈0,1。k=0 表示小 c 先手,k=1 表示小 z 先手;

第二行 n 个整数 a1,a2,…,an。

输出格式

输出一行一个整数表示答案。

样例输入

2 0
1 2

样例输出

1

样例解释

本局小 c 先手,显然他会选择扔掉最靠右的一堆。

子任务

子任务编号n≤特殊性质测试点分值
12020
210^5每堆石子大小相等20
310^5n 是偶数,且小 z 先手20
410^5n 是偶数,且小 c 先手20
520005
610^515

对于 100 数据,1≤n≤10^5,0≤k≤1,ai>0,∑i=1nan≤10^9。


 题目思路:

首先题目不难理解,就是足够聪明的小c要使最后一堆石子数最少,绝顶聪明的小z要使最后一堆石子数最多,两人轮流操作,要么合并,要么扔掉最左边或者最右边。

大家沉浸式想象一下,我们担任小c或者小z中的一员,来体验这个游戏。假设我们就是小z,要尽可能保留最后一堆的石子数最大,但是我们的对手是小c,他不断干扰我们,欲使我们的石子数最少,那我们现在对这个问题的求解就有了新的定义,即要求出最少的最大石子数,最少是前提条件(小c的干扰),最大是我们的任务(小z的任务)。

模拟场景一:我们面前有偶数堆的石子,我们(小z)先操作(k==1),我们肯定会先合并最中间的两堆石子,再静观其变,如果小c扔掉最左边的石子堆,那我们就靠右合并,如果小c扔掉最右边的石子堆,那我们就靠左合并,这样目的是为了时刻保持我们合并的石子堆在最中间,以防被小c扔掉,这样就能保证我们合并的石子数是最大的。如果我们合并的石子堆不是在最中间,就算合并的数量再多,最后还是会被小c扔掉,到头来白打工了。当然,足够聪明的小c也知道你会这么做,他也会控制着自己每次扔掉的是最左边还是最右边,以此来使得你最后合并的数量最小,以此来转化成求解问题,即最小的相邻数段和(因为每次合并都要求是相邻的)。同理,当面对奇数堆的石子,小c先操作(k==0)时,我们已经处在一个最中间的状态,小c扔左,我们合并右,小c扔右,我们合并左。

图解:

场景一代码实现:

int func1(int n)
{
	int ans=0, mid=n/2+1;
	for(int i=1; i<=mid; i++)//最开始的数段和 
	{
		ans=ans+arr[i];
	}
	int temp=ans;
	for(int i=mid+1; i<=n; i++) 
	{
		temp=temp+arr[i]-arr[i-mid];
		ans=min(ans, temp);//不断往后,不断比较迭代,选出最小的 
	}
	return ans;
}

模拟场景二:当我们面对奇数堆的石子,我们(小z)先操作(k==1)或者我们面对偶数堆的石子,小c先操作(k==0)时,即轮到我们(小z)操作的时候,我们面对的永远是奇数堆(因为小c不管是合并还是扔掉,都会是总数减1),我们本已处在最中间最安全的位置,由于我们的操作,会使合并的石子堆位于偏左或偏右的状态,这对于我们来说就是不安全、危险的位置,因为一旦小c发觉你合并的数量多的时候,他就想方设法把你合并的石子堆扔掉。因为你无论怎么合并,小c总有办法把你合并的给扔掉,这样就不能保证合并的石子堆最大并且保留到最后。那怎样才能使得最后的石子堆最大呢?我们知道,当我们面对这种场景二时,小c的操作步数是n/2,即小c只能合并或者扔掉(n/2)次。如果这些个石子堆中有大于(n/2)个x,则小c无论怎样都不能扔完(就算他先合并再扔也相当于操作两次扔两堆),此时能保留到最后一堆的石子数至少有x,所以我们场景二的任务就是要找到最大的那个x,即刚好能满足大于(n/2)的x(刚好等于(n/2)不行,这样会刚好被扔掉)。

场景二代码实现:

bool check(int x, int n)//x的判断
{
	int sum=0, num=0;
	for(int i=1; i<=n; i++)
	{
		sum=sum+arr[i];
		if(sum>=x)
		{
			sum=0;
			num++;
		}
	}
	return num>n/2;//当个数大于n/2时,小c才不会扔完,因为小c只能操作n/2次 
} 
int func2(int n)
{
	int r=1e9, mid=r/2;
	int ans=0;
	while(mid<=r)//用二分法不断逼近刚好能达到check条件的x
	{
		if(check(mid, n))
		{
			ans=mid;
			if(mid==r)return ans;//如果相等且满足,说明最大值r(mid)就是ans
			mid=(mid+1+r)/2;
		}
		else 
		{
			if(mid==r)break;//如果相等且不满足,说明ans就是上一次的mid,直接退出 
			r=mid-1;
			mid=r/2;
		}
	}
	return ans;
}

AC代码:

#include<iostream>
using namespace std;
int arr[100001]={0};
int func1(int n)
{
	int ans=0, mid=n/2+1;
	for(int i=1; i<=mid; i++)//最开始的数段和 
	{
		ans=ans+arr[i];
	}
	int temp=ans;
	for(int i=mid+1; i<=n; i++) 
	{
		temp=temp+arr[i]-arr[i-mid];
		ans=min(ans, temp);//不断往后,不断比较迭代,选出最小的 
	}
	return ans;
}
bool check(int x, int n)//x的判断
{
	int sum=0, num=0;
	for(int i=1; i<=n; i++)
	{
		sum=sum+arr[i];
		if(sum>=x)
		{
			sum=0;
			num++;
		}
	}
	return num>n/2;//当个数大于n/2时,小c才不会扔完,因为小c只能操作n/2次 
} 
int func2(int n)
{
	int r=1e9, mid=r/2;
	int ans=0;
	while(mid<=r)//用二分法不断逼近刚好能达到check条件的x
	{
		if(check(mid, n))
		{
			ans=mid;
			if(mid==r)return ans;//如果相等且满足,说明最大值r(mid)就是ans 
			mid=(mid+1+r)/2;
		}
		else 
		{
			if(mid==r)break;//如果相等且不满足,说明ans就是上一次的mid,直接退出 
			r=mid-1;
			mid=r/2;
		}
	}
	return ans;
}
int main() 
{
	int n=0, k=0;
	scanf("%d%d", &n, &k);
	for(int i=1; i<=n; i++) 
	{
		scanf("%d", &arr[i]);
	}
	int ans=0;
	if(((n%2==0)&&k==1)||(n%2==1)&&k==0)ans=func1(n);//小z面对偶数局面 
	else ans=func2(n);//小z面对奇数局面 
	printf("%d", ans);
	return 0;
}

希望能帮助到大家!

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值