题目背景
小 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≤ | 特殊性质 | 测试点分值 |
---|---|---|---|
1 | 20 | 无 | 20 |
2 | 10^5 | 每堆石子大小相等 | 20 |
3 | 10^5 | n 是偶数,且小 z 先手 | 20 |
4 | 10^5 | n 是偶数,且小 c 先手 | 20 |
5 | 2000 | 无 | 5 |
6 | 10^5 | 无 | 15 |
对于 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;
}
希望能帮助到大家!