取石子 ,但是作弊 link
题目描述
你明天要和小A玩取石子游戏了,游戏规则如下:有n堆石子,每堆有a[i]个石子。小A先手,你俩轮流取,可以从任意不为空一堆里取出任意多个。谁取到了最后一个石子谁就赢了。
显然,这个游戏对先手的小A很有利。所以你决定在今天晚上偷偷地从第一堆里取出一些石子放到另一堆里使得后手必胜,现在问你可以取出几个石子来达成目的。如果有多种方案,输出取出最少石子数量。
(注意,你只能从第一堆移动到另一堆而不能移动到多个堆里。同时为了避免被发现,你不能取完第一堆里全部的石子导致总堆数减少。可以不移动)
输入描述:
第一行输入一个正整数 n,表示有 n 堆石子(1≤n≤1000)。
接下来 1 行 n 个正整数分别表示第 i 堆石子的数量(1≤ai≤1000)。
输出描述:
如果可以按规则移动后使后手必胜,输出一个非负整数xxx表示你的答案(0≤x<a1)
如果怎样作弊也不能后手必胜,则输出一个−1-1−1。
示例1
输入
1
3
输出
-1
说明
这个状态先手必胜,而且你无法移动,所以只好认输
示例2
输入
3
6 3 1
输出
1
说明
仅有的两种方案是移动1个或2个到第二堆里,所以输出1
题解
博弈论基本是不会的,都是上网现学的x(一定不是之前懒得看嗯嗯)
本题用到的是Nim(尼姆)博弈!如果没学过的话可以看下这个博客 尼姆博弈浅谈(Nim) 感觉讲的蛮透彻的~~(嗯…考试的时候基本上能现看懂大部分)~~
看完理解之后会的出来一个很有用的结论A:n堆石子的个数相互异或之后若为零则先手必胜,若不为零则先手必输
异或知识点:
⊕ xor ^(有时)都是异或运算
二进制位上两数不同为1 相同为2
0^0 = 0 1^1 = 0 1^0 = 1 0^1 = 1
然后再来看题 (咱要开始作弊啦!)
现在这个题就可以分成三个状况
1.只有1堆石子!先手可以一下子拿完,咱赢不了呜呜(这到底是人性的扭曲还是道德的沦丧!
2. 每个a[i](每堆石子个数)异或之后为零,那么我们是后手,不管怎么玩都赢定了!这个时候就不用动啦,输出0就好了。
3. 每个a[i](每堆石子个数)异或之后不为零!这可不行 咱得作弊赢了他!
所以我们在今天晚上偷偷地从第一堆里取出一些石子放到另一堆里使得我们必胜,就是让改变之后的 每堆石子个数的异或为零 根据上面的结论A可以知道我们就可以赢!
那要怎么分呢?
我们很容易想到一个方法:将第一组石子分别取出 1-(第一组石子数-1)个石子 放到后面2-n堆石子的某一堆中,然后算出改变之后每堆石子个数的异或 如果出现了0,那么就跳出来输出当前从第一堆里取的石子数,如果一直不出现零,那就是我们怎么搞都不能赢辽…
思路应该还是蛮清晰的叭…但是!我们会发现这样做的话会有三个循环嵌套在一起
循环1.枚举从第一组取的石子个数a[i]
循环2.枚举要放石子的堆的编号n
循环3.重新异或一遍n)时间复杂度最高会达到惊人的n*n*x!
再看眼数据是1000,那肯定不行!必须要优化一下
考虑到1 2是必要的基本优化不了(也可能是我想不到qwq),我们看到三的异或操作,会想到异或会不会有什么特殊的性质可以优化呢?
我们の心路历程:第一遍算原始数据的异或的时候,已经算出来了一个值,而改变后重新算出来的的异或值只是在原来的基础上异或去掉了原始的a[1] 和a[i](a[i]是当前循环2枚举到的石子堆的石子数) 然后异或上了改变之后的a[1] 和a[i],所以我们会去想如果异或有一种方法,可以去掉原来异或上的值,那么循环三可以由n次操作缩短到简简单单的几次操作!省掉了一次循环,大大减少了我们滴时间!
所以当时就去现搜了一下有没有这种方法,很遗憾我没有搜到
!但是!自己当时给穷举出来辣!哈哈哈哈哈(x
异或性质 一个数异或同一个数两次 结果还是原来那个数 相当于 a ^ b ^ b = a
穷举
1 ^ 1 ^ 1 = 1;
1 ^ 0 ^ 0 = 1;
0 ^ 0 ^ 0 = 0;
0 ^ 1 ^ 1 = 0;
这个是每一位上的,然后可以推广到普通的多位二进制数
思路讲完辣,下面是ac代码
(注意五角星地方,当时因为这个wa了几次orz)
AC代码
#include<cstdio>
using namespace std;
const int N = 1005;
int a[N];
int main()
{
int n;
scanf("%d", &n);
int x = 0;//初始为0 是因为 0和a都是a,保证不会被打扰到
for(int i = 1; i<=n;i++)
{
scanf("%d", &a[i]);
x = x^a[i];//求所有堆的异或
}
if(n == 1)
{//状况1 只有1堆石子!先手可以一下子拿完,咱赢不了呜呜
printf("-1\n");
return 0;
}
else if(x == 0)
{// 状况2 每个a[i](每堆石子个数)异或之后为零,那么我们是后手,不管怎么玩都赢定了!这个时候就不用动啦,输出0就好了。
printf("0");
return 0;
}
else
{//每个a[i](每堆石子个数)异或之后不为零!开始作弊
for(int i = 0;i<a[1];i++)// 循环1.枚举从第一组取的石子个数a[i]
{
int xx = x;
xx = xx^a[1];// 用异或运算清掉原来异或结果中的a[1]
a[1] -= i;// 算出来新的a[1]
xx = xx^a[1];// 异或算上新的a[1]
for(int j = 2;j<=n;j++)// 循环2.枚举要放石子的堆的编号n
{
int temp = xx;
temp = temp^a[j];// 用异或运算清掉原来异或结果中的a[j](第j堆石子的数)
a[j] += i;// 算出来新的a[j]
temp = temp^a[j];// 异或算上新的a[1]
a[j] -= i;// ★★★★★ 记得把第j堆数组还原回来
if(temp == 0)
{ // 如果出现了0,那么就跳出来输出当前从第一堆里取的石子数
printf("%d\n", i);
return 0;
}
}
a[1] += i;
}
}
// 如果一直不出现零,那就是我们怎么搞都不能赢辽
printf("-1\n");
return 0;
}
碎碎念:当时打这个比赛的时候就搞出来两道签到+这一道(我也不知道算不算签到)的题,其他的题看上去都不难但是赛后补题的时候发现基本都不大会呜呜 最后也就350左右 唉…acm真的大佬好多…不知道我还能撑多久…看着身边的人都跑掉了但是自己还没有什么其他的方向,比较迷茫…只能先在这里浑浑噩噩的过些日子叭…
493






