知识基础:Nim游戏与SG函数
参考:[学习笔记] (博弈论)Nim游戏和SG函数_A_Comme_Amour的博客-CSDN博客_nim博弈
首先定义P是先手必败,N是先手必胜
有以下性质:
那么终结位置是P
可以转移到P的是N
所有操作都到达N的是P
那么根据以上三点,可以暴力求某一个局面是N还是P。步骤是先使得终结位置是P,然后枚举状态,能到P的是N。然后所有都到达N的P。然后不断迭代。
我做过的一些博弈题就是这个思路。
但是Nim游戏这么做的话时间复杂度很高。
Nim游戏的结论是异或和为0的情况下先手必败。
异或和为0和异或和不为0都可以通过一步操作互相转化。
sg函数:定义mex()表示集合中最小的为出现的非负整数。sg[x] = mex(sg[y] | y是x的后继)
那么终结位置没有后继,sg为0,其他的值可以通过定义算出。
sg = 0是先手必败,否则先手必胜。
多个子游戏的sg值是它们的异或和。
sg的计算:一堆石子,若操作石子数为1~m,那么sg[x] = x % (m + 1)
若为非零任意数,那么sg[x] = x
其他,根据定义暴力计算即可。
例题
比较裸,看作多个子游戏,每个子游戏计算sg值,看异或和即可
阶梯博弈
首先偶数层的石子是没有用的,因为一个人移动了偶数层的石子,另一个人可以模仿把移到奇数层的又移到偶数层。奇数层的操作,石子到了偶数层,相当于扔掉了。所以可以只看奇数层,奇数层的石子组成了一个Nim游戏。对于有必胜策略的那个人,对面移动奇数堆,我就按照Nim游戏移,对面移偶数堆,我就模仿它。
Great Party(Nim拓展+莫队)
在nim游戏的基础上,加了一个可以把剩余石子合并到其他堆的操作
如果是1堆,先手直接拿完,先手必胜
如果是两堆,从简单的开始。1 1,那么每次操作没得选,后手赢。
1 x,x>1 先手可以转化到1 1,先手赢
x x 这时每次操作都不能合并,合并就输了,这时后手可以模仿先手的操作,最后后手拿完,后手赢。
x y ,x!= y 先手可以转化到x x,先手赢
总结,2堆的时候,相同后手赢,不相同先手赢
3堆,发现这时先手可以转化为x x的情况,先手赢
4堆,这时一定不能合并,一合并就变成三堆,对手面对这种情况必赢
所以最后一定是1 1 1 1。那么每个数减去1,就变成了标准的nim游戏了
此时若异或和为0,后手操作完,先手面对1 1 1 1,此时后手赢。反之,异或和不为0,先手1
因此4堆的情况,减去1后,异或和为 0,后手赢,异或和不为0,先手赢。我们可以发现这个结论是包含2堆的情况的。
因此我们可以猜结论,奇数堆是先手,偶数堆,减去1后,异或和不为0是先手,否则后手。
为了简便,我们计算后手赢的情况,也就是长度为偶数且异或和为0
因为异或和不为0不好判断,异或和为0的话转化成前缀也就是前缀异或和相同,这个可以用莫队。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
struct query
{
int l, r, id, bl;
}q[N];
int s[N], n, m;
LL ans[N], cur;
unordered_map<int, int> cnt[2];
bool cmp(query x, query y)
{
if(x.bl != y.bl) return x.bl < y.bl;
if(x.bl % 2 == 1) return x.r < y.r;
return x.r > y.r;
}
void add(int x)
{
int id = x % 2;
x = s[x];
cur += cnt[id][x];
cnt[id][x]++;
}
void erase(int x)
{
int id = x % 2;
x = s[x];
cnt[id][x]--;
cur -= cnt[id][x];
}
int main()
{
scanf("%d%d", &n, &m);
int block = sqrt(n);
_for(i, 1, n)
{
int x; scanf("%d", &x);
x--;
s[i] = s[i - 1] ^ x;
}
_for(i, 1, m)
{
int l, r;
scanf("%d%d", &l, &r);
l--;
q[i] = {l, r, i, l / block};
}
sort(q + 1, q + m + 1, cmp);
int l = 1, r = 0;
_for(i, 1, m)
{
int ll = q[i].l, rr = q[i].r;
while(l < ll) erase(l++);
while(l > ll) add(--l);
while(r > rr) erase(r--);
while(r < rr) add(++r);
LL len = rr - (ll + 1) + 1;
ans[q[i].id] = len * (len + 1) / 2 - cur;
}
_for(i, 1, m) printf("%lld\n", ans[i]);
return 0;
}
Z-Game on grid(dp)
这个不是标准的博弈,但是很像博弈。A可以控制自己的行为,B可以理解为随机走。
从结果逆推,当A操作时,只要有一个操作可以达到即可,B操作时,要全部都达到
有点像暴力算N状态和P状态
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 500 + 10;
int dp[N][N][3], n, m; //0 A 1 D 2 B
char s[N][N];
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%s", s[i]);
_for(i, 1, n + 1)
_for(j, 1, m + 1)
rep(k, 0, 3)
dp[i][j][k] = 0;
for(int i = n; i >= 1; i--)
for(int j = m; j >= 1; j--)
{
if(s[i][j] == 'A') dp[i][j][0] = 1;
else if(s[i][j] == 'B') dp[i][j][2] = 1;
else if(i == n && j == m) dp[i][j][1] = 1;
else
{
rep(k, 0, 3)
{
if((i + j) % 2 == 0) dp[i][j][k] = (dp[i + 1][j][k] || dp[i][j + 1][k]);
else dp[i][j][k] = ((i + 1 > n || dp[i + 1][j][k]) && (j + 1 > m || dp[i][j + 1][k]));
}
}
}
rep(k, 0, 3) printf(dp[1][1][k] ? "yes " : "no ");
puts("");
}
return 0;
}
Split Game(暴力计算SG函数)
这题就是暴力计算SG函数
先把边界条件,sg=0先手必败初始化后
然后遍历每一个状态,对于每一个状态枚举它的所有后继,计算后继的sg值
这道题的操作是分解成两个子游戏,这时的sg值是它们的异或。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 160;
int sg[N][N], a[N << 1];
int check(int i, int j)
{
return i == 1 && j == 1;
}
int main()
{
memset(sg, -1, sizeof sg);
sg[1][2] = sg[2][1] = 0;
sg[1][3] = sg[3][1] = 0;
_for(i, 1, 150)
_for(j, 1, 150)
{
if(i == 1 && j == 1 || !sg[i][j]) continue;
memset(a, 0, sizeof a);
_for(k, 1, i - 1)
{
if(check(k, j) || check(i - k, j)) continue;
a[sg[k][j] ^ sg[i - k][j]] = 1;
}
_for(k, 1, j - 1)
{
if(check(i, k) || check(i, j - k)) continue;
a[sg[i][k] ^ sg[i][j - k]] = 1;
}
rep(k, 0, N << 1)
if(!a[k])
{
sg[i][j] = k;
break;
}
}
int n, m;
while(~scanf("%d%d", &n, &m))
puts(sg[n][m] ? "Alice" : "Bob");
return 0;
}
P1290 欧几里德的游戏(SG函数)
N状态和P状态和SG函数的0和1是一致的,多个游戏的时候SG就不只是1,要算异或值。而只有一个游戏的时候可以看作只有0和1,0是先手必败
这题就算SG函数集合
对于n > m
sg(n, m) = mex{sg(n - m, m), sg(n - 2m, m)……sg(m, n % m)}
当sg(m, n% m) = 0时,显然sg(n, m) = 1
当sg(m, n % m) = 1 时,sg(n % m + m, m) = 0,其他为0
边界情况是m=0时sg为0,这时先手必败
因此递归计算sg函数即可
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
int dfs(int n, int m)
{
if(m == 0) return 0;
int t = dfs(m, n % m);
if(!t) return 1;
else
{
if(n / m == 1) return 0;
else return 1;
}
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
int n, m;
scanf("%d%d", &n, &m);
if(n < m) swap(n, m);
puts(dfs(n, m) ? "Stan wins" : "Ollie wins");
}
return 0;
}
hdu 1404(打表)
直接根据P态和N态打表,P是先手必败
可以到达P态的是N态。不能N态的是P态
最优策略就是当前是P态,那么不管怎么走都是到N态,当前是N态,那么必然走P态
我开始用string, cin输入然后T了
要用字符数组。
算的时候转化成数字,注意前导零是一个坑,有前导零和没有前导零完全不同,有前导零先手必胜。
我写的打表方式是由之前的状态推现在的状态
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
int sg[N];
void cal(int x) //1先手必胜 0先手必败
{
for(int i = 1; i <= x; i *= 10)
{
int cur = x / i % 10;
if(cur)
{
int t = x - cur * i;
_for(j, 0, cur - 1)
{
if(i * 10 > x && j == 0) continue;
if(!sg[t + j * i])
{
sg[x] = 1;
return;
}
}
}
else if(!sg[x / i / 10])
{
sg[x] = 1;
return;
}
}
sg[x] = 0;
}
int main()
{
sg[0] = 1;
rep(i, 1, 1e6) cal(i);
char s[10];
while(~scanf("%s", s))
{
if(s[0] == '0') puts("Yes");
else puts(sg[stoi(s)] ? "Yes" : "No");
}
return 0;
}
还有一种写法是,开始是0,然后能到它的是1,然后一直往后。如果已经是1了就跳过,如果是0就继续往后拓展。也就是递推
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
int sg[N];
void deal(int x)
{
//第一种操作
for(int i = 1; i <= x; i *= 10)
if(x / i % 10 < 9)
{
int t = x;
while(1)
{
t += i;
sg[t] = 1;
if(t / i % 10 == 9) break;
}
}
// 第二种操作
x *= 10;
int t = 1;
while(1)
{
rep(i, 0, t)
{
if(x * t + i >= 1e6) return;
sg[x * t + i] = 1;
}
t *= 10;
}
}
int main()
{
sg[0] = 1;
rep(i, 1, 1e6)
if(!sg[i])
deal(i);
char s[10];
while(~scanf("%s", s))
{
if(s[0] == '0') puts("Yes");
else puts(sg[stoi(s)] ? "Yes" : "No");
}
return 0;
}
hdu 1079(打表)
和上一题类似,打表即可
用记忆化搜索
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
unordered_map<int, int> sg;
int m[15];
int check(int year)
{
return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
}
struct node
{
int year, month, day;
int get()
{
return year * 1e4 + month * 1e2 + day;
}
};
int m_max(int year, int month)
{
if(check(year) && month == 2) return 29;
return m[month];
}
node add1(int year, int month, int day)
{
day++;
if(day > m_max(year, month))
{
day = 1;
month++;
if(month == 13)
{
month = 1;
year++;
}
}
return {year, month, day};
}
node add2(int year, int month, int day)
{
int next = month + 1;
if(next == 13) next = 1;
if(day <= m_max(year, next))
{
month++;
if(month == 13)
{
month = 1;
year++;
}
return {year, month, day};
}
return {-1, month, day};
}
bool pd(node t)
{
return t.get() <= node{2001, 11, 4}.get();
}
int dfs(node cur)
{
if(sg[cur.get()]) return sg[cur.get()];
node t = add1(cur.year, cur.month, cur.day);
if(pd(t) && dfs(t) == -1) return sg[cur.get()] = 1;
t = add2(cur.year, cur.month, cur.day);
if(t.year != -1 && pd(t) && dfs(t) == -1) return sg[cur.get()] = 1;
return sg[cur.get()] = -1;
}
void init()
{
_for(i, 1, 12) m[i] = 31;
m[4] = m[6] = m[9] = m[11] = 30;
m[2] = 28;
sg[node{2001, 11, 4}.get()] = -1;
}
int main()
{
init();
int T; scanf("%d", &T);
while(T--)
{
int year, month, day;
scanf("%d%d%d", &year, &month, &day);
puts(dfs({year, month, day}) == 1 ? "YES" : "NO");
}
return 0;
}
小牛再战(打表+猜结论)
三个步骤
1.打表:手算或程度
2.从表中猜结论,找规律
3.证明结论
这题其实程序打表很复杂,因为状态转移非常多,所以手算
n = 0 P
n = 1 N
n = 2 相等时,先手一定破坏相等,后手一定可以操作到相等。最后是后手拿完,P
不相等时,先手可以操作到相等,N
n = 3 设x1 >= x2 >= x3 显然可以操作x1使得有x2 x2 即操作到P态, 那么当前是N态
n = 4 这时就比较困难了,根据前面的状态猜结论
n为奇数N态,n为偶数,两两相等则P否则N。也可以猜n为偶数全部相等则P,但可以举出反例,入2 2 1 1
证明用P态和N态的性质证明
1.首先终止状态全0满足两两相等,即P态。
2.其次证明P态一定走到N态
对于两两相等的,不管怎么操作一定不会再两两相等。
首先不能放石子给其他堆,否则破坏其他堆的配对。不能放时,一定破坏当前的配对
3.最后证明N态可以走到P态
当N为奇数时,即x1 >= x2 ……xn
我们假设可以配对成x2 x2 x4 x4……xn xn
需要的石子数为x2 - x3 + x4 - x5 ……xn-1 - xn
写为 x2 +(x4 - x3) + ……(xn-1 - xn-2) - xn
括号里都是小于等于0的,最后是小于0的,所以整个式子是小于x2的
因此x1是大于上式的,所以一定可以提供这么多。
当N为偶数时,x1 x2……xn
那么假设x1拿走一部分,使得为
xn x2 x2 x4 x4……xn
那么即证明x1 - xn > x2 -x3 + x4 - x5 ……xn-2 - xn-1
即x1 > x2 +(x4 - x3) + ……(xn-1 - xn)
括号里都是小于等于,只要有一个是小于,那就成立
不成立的情况只有两两配对的情况,这个情况不是N态。
因此得证。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 15;
int a[N], n;
int main()
{
while(scanf("%d", &n) && n)
{
_for(i, 1, n) scanf("%d", &a[i]);
if(n % 2 == 1) puts("Win");
else
{
sort(a + 1, a + n + 1);
int flag = 0;
for(int i = 1; i <= n; i += 2)
if(a[i] != a[i + 1])
{
flag = 1;
break;
}
puts(flag ? "Win" : "Lose");
}
}
return 0;
}
cf 1537D(打表+猜结论)
打个表,发现0和1交替,但有一些反例
于是就猜偶数是1,奇数是0
打出反例发现是2 8 32……
特判一下反例即可。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
map<ll, int> mp;
int main()
{
ll cur = 2;
while(1)
{
mp[cur] = 1;
cur *= 4;
if(cur > 1e9) break;
}
int T; scanf("%d", &T);
while(T--)
{
int x; scanf("%d", &x);
int cur = (x % 2 == 0) ? 1 : 0;
if(mp[x]) cur ^= 1;
puts(cur ? "Alice" : "Bob");
}
return 0;
}
反常Nim游戏
nim游戏结论是异或和为0是P态,否则N态
而反常nim游戏就是终态是反的,即拿了最后一个石子的人输
这个时候分两种情况,全1的话,奇数是P态,偶数是N态。不全为1的话,和nim游戏一样
hdu 1730(Nim游戏或SG函数)
这是一个不平等游戏,即两个人可操作的集合是不同的。这题黑子和白子显然可操作的是不同的
这道题有两种理解方式
Nim游戏的话,用每一行中间的差值看作一堆石子即可
证明:
1.当全部为0,即全部挨在一起时,是P态,因为后手可以一直移动到挨着。
2.P态一定走到N态:一个异或值为0的序列,改变其中一个数,不管变大或者变小,一定改变后异或值不为0
3.N态可以走到P态。一定可以通过减少某个xi达到异或值为0(证明Nim游戏时已证)
因此就可以看作Nim游戏。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
int main()
{
int n, m;
while(~scanf("%d%d", &n, &m))
{
int cur = 0;
_for(i, 1, n)
{
int x, y;
scanf("%d%d", &x, &y);
cur ^= abs(x - y) - 1;
}
puts(cur ? "I WIN!" : "BAD LUCK!");
}
return 0;
}
栗酱的异或和(Nim游戏扩展)
Nim游戏基础上加了一个限制,即从第k堆开始取
其实就看从第k堆开始取能否使异或和为0即可
此时要求a[k] > x ^ a[k]
x是异或和
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int a[N], n, k;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d%d", &n, &k);
int x = 0;
_for(i, 1, n)
{
scanf("%d", &a[i]);
x ^= a[i];
}
puts((x && a[k] > (x ^ a[k])) ? "Yes" : "No"); //这里注意要加一些括号 因为这个运算优先级WA了一发
}
return 0;
}
Georgia and Bob(阶梯Nim)
阶梯博弈算法详解(尼姆博弈进阶)_我爱AI_AI爱我的博客-CSDN博客_尼姆博弈 算法
阶梯nim可以看作很多个石子堆,每次操作是将第i堆的石子移动到第i-1堆,第1堆可以任意减少。不能操作的人输
这时把奇数堆提取出来,然后看作nim游戏即可。因为先手可以按照nim操作,后手如果移动偶数堆的,先手模仿它即可,不影响结果,因此可以排除掉偶数堆的影响。
对于这题而言,把每个棋子可以移动的空格算出,发现一移动,相邻的两个一加一减,相当于把石子移动到相邻一堆,于是就刚好符合阶梯Nim的规则
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
int a[N], n;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
vector<int> ve;
_for(i, 1, n) scanf("%d", &a[i]);
sort(a + 1, a + n + 1);
for(int i = n; i >= 1; i--)
ve.push_back(a[i] - a[i - 1] - 1);
int x = 0;
for(int i = 0; i < ve.size(); i += 2) x ^= ve[i];
puts(!x ? "Bob will win" : "Georgia will win");
}
return 0;
}
Rake It In(minmax搜索)
学校课程学过这个方法,搜索即可
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
struct node{ int a[5][5]; };
node make(node x, int i, int j)
{
int t = x.a[i][j];
x.a[i][j] = x.a[i][j + 1];
x.a[i][j + 1] = x.a[i + 1][j + 1];
x.a[i + 1][j + 1] = x.a[i + 1][j];
x.a[i + 1][j] = t;
return x;
}
int dfs_max(node x, int k);
int dfs_min(node x, int k)
{
int res = 1e9;
_for(i, 1, 3)
_for(j, 1, 3)
{
int get = x.a[i][j] + x.a[i + 1][j] + x.a[i][j + 1] + x.a[i + 1][j + 1];
res = min(res, get + dfs_max(make(x, i, j), k - 1));
}
return res;
}
int dfs_max(node x, int k)
{
if(!k) return 0;
int res = 0;
_for(i, 1, 3)
_for(j, 1, 3)
{
int get = x.a[i][j] + x.a[i + 1][j] + x.a[i][j + 1] + x.a[i + 1][j + 1];
res = max(res, get + dfs_min(make(x, i, j), k));
}
return res;
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
int k; scanf("%d", &k);
node x;
_for(i, 1, 4)
_for(j, 1, 4)
scanf("%d", &x.a[i][j]);
printf("%d\n", dfs_max(x, k));
}
return 0;
}
Palindrome Game (hard version)(dp)
这题类似博弈,但不是博弈。用dp解决。当前状态可以由00 01 中间是否有0 上一次是否翻转来表示,一个状态的值为自己的代价减去对方的代价。
然后分类讨论dp即可
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
int dp[N][N][2][2], a[N], n;
int main()
{
memset(dp, 0x3f, sizeof dp);
dp[0][0][0][0] = dp[0][0][0][1] = 0;
_for(i, 0, 1e3)
_for(j, 0, 1e3)
_for(p, 0, 1)
for(int r = 1; r >= 0; r--) //这里注意顺序 因为0是从1转移过来的
{
if(i > 0) dp[i][j][p][r] = min(dp[i][j][p][r], 1 - dp[i - 1][j + 1][p][0]);
if(j > 0) dp[i][j][p][r] = min(dp[i][j][p][r], 1 - dp[i][j - 1][p][0]);
if(p == 1) dp[i][j][p][r] = min(dp[i][j][p][r], 1 - dp[i][j][0][0]);
if(r == 0 && j > 0) dp[i][j][p][r] = min(dp[i][j][p][r], -dp[i][j][p][1]); //记住不为回文串才可以反转
}
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) scanf("%1d", &a[i]);
int i = 0, j = 0, p = 0, r = 0;
_for(l, 1, n)
{
int r = n - l + 1;
if(l >= r) break;
if(a[l] + a[r] == 0) i++;
if(a[l] + a[r] == 1) j++;
}
if(n % 2 == 1 && a[(n + 1) / 2] == 0) p = 1;
if(dp[i][j][p][r] < 0) puts("Alice");
else if(dp[i][j][p][r] > 0) puts("BOB");
else puts("DRAW");
}
return 0;
}
A Chess Game(SG函数+拓扑排序)
SG函数裸题,拓扑排序使得求SG函数的顺序正确
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
vector<int> g[N], topo;
unordered_map<int, bool> vis;
int d[N], sg[N], n, m;
int main()
{
scanf("%d", &n);
rep(u, 0, n)
{
int k; scanf("%d", &k);
while(k--)
{
int v; scanf("%d", &v);
g[u].push_back(v);
d[v]++;
}
}
queue<int> q;
rep(i, 0, n)
if(!d[i])
q.push(i);
while(!q.empty())
{
int u = q.front(); q.pop();
topo.push_back(u);
for(int v: g[u])
if(--d[v] == 0)
q.push(v);
}
for(int i = n - 1; i >= 0; i--)
{
vis.clear();
int u = topo[i];
for(int v: g[u]) vis[sg[v]] = 1;
rep(j, 0, n)
if(!vis[j])
{
sg[u] = j;
break;
}
}
while(scanf("%d", &m) && m)
{
int cur = 0;
_for(i, 1, m)
{
int x; scanf("%d", &x);
cur ^= sg[x];
}
puts(cur ? "WIN" : "LOSE");
}
return 0;
}
S-Nim(SG函数)
暴力计算每个子游戏SG函数,异或起来即可
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e4 + 10;
int sg[N], s[N], n, k, m;
unordered_map<int, bool> vis;
int main()
{
scanf("%d", &k);
_for(i, 1, k) scanf("%d", &s[i]);
_for(i, 0, 1e4)
{
vis.clear();
_for(j, 1, k)
if(i - s[j] >= 0)
vis[sg[i - s[j]]] = 1;
rep(j, 0, 1e4)
if(!vis[j])
{
sg[i] = j;
break;
}
}
scanf("%d", &m);
while(m--)
{
int l, x, cur = 0;
scanf("%d", &l);
_for(i, 1, l)
{
scanf("%d", &x);
cur ^= sg[x];
}
printf(cur ? "W" : "L");
}
return 0;
}
Stone Game(SG函数+找规律)
先手算SG函数,注意计算SG函数时从游戏结束开始倒推,这道题游戏结束都是箱子满了的时候,从这里开始反过来推一下。
数据范围不允许暴力,那就找一下规律即可
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
int find(int x)
{
_for(t, 0, 1e4)
if(t + t * t < x && (t + 1) + (t + 1) * (t + 1) >= x)
return t;
return 0;
}
int main()
{
int n, cur = 0;
scanf("%d", &n);
_for(i, 1, n)
{
int s, c;
scanf("%d%d", &s, &c);
if(c == 0) continue;
vector<int> ve;
while(s)
{
ve.push_back(s);
s = find(s);
}
sort(ve.begin(), ve.end());
rep(i, 0, (int)ve.size())
if(ve[i] <= c && c <= ve[i + 1])
{
if(ve[i] == c) cur ^= 0;
else cur ^= ve[i + 1] - c;
break;
}
}
puts(cur ? "Yes" : "No");
return 0;
}
Be the Winner(反常Nim)
这题不太一样,最后取的人输,注意这样不能用SG函数,SG函数默认是正常游戏
这是经典的反常Nim,只要全为1特判一下,其他的和Nim游戏一样。
注意这题还可以把石子堆拿掉一些,然后把剩余的分两堆。可以分析一下,对于全1的不能分开,不为全1的,从异或值不为0到为0,不需要这个操作,从异或值为0到异或值不为0,有了这个操作也没用。所以这个操作对结果没用影响。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
int main()
{
int n, cur = 0, flag = 1;
scanf("%d", &n);
_for(i, 1, n)
{
int x; scanf("%d", &x);
if(x > 1) flag = 0;
cur ^= x;
}
if(flag) puts((n % 2 == 0) ? "Yes" : "No");
else puts(cur ? "Yes" : "No");
return 0;
}
[HNOI2007]分裂游戏(隐藏nim)
有很多隐藏的nim游戏
1.1个1xn的棋盘,有很多棋子,每个人可以把棋子往左移动任意步数,一个格子可以有多个棋子
实际上,每个棋子可以走的步数看作一个石子堆,就成了nim游戏
2.有n堆石子,一个人可以把左边堆的一个石子放到右边的堆中。把这个石子堆看作棋盘,发现就和上面的一样了。只不过算sg值的时候,相同的会异或完,所以一个格子的棋子数是奇数才对最终的sg值起作用。另一个角度,后手可以模仿前手的操作,石子两两抵消。
3.也就是这道题,上面两个懂了,这个就很容易了,也就是看作棋盘。其实就是将1个石子堆分裂成两个大小小于它的石子堆,暴力sg函数求即可。
第一次操作需要将异或和不为0变成为0,此时注意不考虑棋子数的奇偶,只要满足这个操作即可,棋子的奇偶是对于最后的sg值来说的。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 30;
int sg[N], p[N], n, ans1, ans2, ans3;
unordered_map<int, bool> vis;
int main()
{
_for(x, 1, 25)
{
vis.clear();
_for(i, 0, x - 1)
_for(j, 0, x - 1)
vis[sg[i] ^ sg[j]] = 1;
int t = 0;
while(vis[t]) t++;
sg[x] = t;
}
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &p[i]), p[i] %= 2;
int x = 0;
_for(i, 1, n)
if(p[i])
x ^= sg[n - i];
if(!x)
{
puts("-1 -1 -1\n0");
continue;
}
int cnt = 0, fi = 1;
_for(i, 1, n)
_for(j, i + 1, n)
_for(k, j, n)
if((sg[n - i] ^ sg[n - j] ^ sg[n - k]) == x)
{
cnt++;
if(fi)
{
fi = 0;
ans1 = i, ans2 = j, ans3 = k;
}
}
printf("%d %d %d\n%d\n", ans1 - 1, ans2 - 1, ans3 - 1, cnt);
}
return 0;
}
A tree game(Hackenbush模板题)
一颗树,在上面删边,删完后它的子树也全没了,谁不能操作谁输。
结论是多一条边sg值+1,两条链的sg值看作两个游戏,异或起来
用sg[u]表示u为跟的子树的sg值即可
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
vector<int> g[N];
int sg[N], n;
void dfs(int u, int fa)
{
sg[u] = 0;
for(int v: g[u])
{
if(v == fa) continue;
dfs(v, u);
sg[u] ^= sg[v] + 1;
}
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) g[i].clear();
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
puts(sg[1] ? "Alice" : "Bob");
}
return 0;
}
Christmas Game(树上博弈+阶梯nim+换根dp)
这题好秀啊
自己想的时候,先思考一条链的情况,发现就是阶梯nim,但是不知道怎么转化到树上
我是以合并的思路来考虑的,实际上并不能看作多个子游戏
实际上不是合并,而是直接思考,其实阶梯nim的时候,奇数堆就是一堆石子,那么树上也一样,奇数深度是石子,把它们全部异或起来即可。深度定义为除以k向下取整
那么接下来怎么换根dp呢
因为又要除以k,又要奇数,所以换一种等价的更容易考虑的方式,即模2k为k~2k-1的,这个用换根比较好维护。
于是先预处理dp[u][j]表示以u为根的子树,距离u模2k为j的节点的异或和,这个用一个dp维护一下即可。
然后在换根dp的时候,要用到dp[u]和dp[v],dp[u]要先减去v部分的贡献,然后再把u加到v上,使得dp[v]变成以v为根节点。然后注意恢复现场
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int dp[N][45], sg[N], a[N], n, k;
vector<int> g[N];
void dfs(int u, int fa)
{
dp[u][0] = a[u];
for(int v: g[u])
{
if(v == fa) continue;
dfs(v, u);
rep(j, 0, k) dp[u][(j + 1) % k] ^= dp[v][j];
}
}
void dfs2(int u, int fa)
{
_for(i, k / 2, k - 1) sg[u] ^= dp[u][i];
for(int v: g[u])
{
if(v == fa) continue;
rep(j, 0, k) dp[u][(j + 1) % k] ^= dp[v][j];
rep(j, 0, k) dp[v][(j + 1) % k] ^= dp[u][j];
dfs2(v, u);
rep(j, 0, k) dp[v][(j + 1) % k] ^= dp[u][j];
rep(j, 0, k) dp[u][(j + 1) % k] ^= dp[v][j];
}
}
int main()
{
scanf("%d%d", &n, &k);
k *= 2;
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
_for(i, 1, n) scanf("%d", &a[i]);
dfs(1, 0);
dfs2(1, 0);
_for(i, 1, n) printf("%d ", sg[i] > 0);
return 0;
}
二分图博弈
在一个二分图上,从一个起点开始,每次可以沿着边走到另外一个点,走过的点不能再走,不能移动的人输。
结论是如果起点一定在最大匹配边上,则先手必胜,否则先手必败。
可以去掉起点跑最大流,然后加入起点在残量网络上再跑,如果最大流增加则一定在最大匹配边上。
斐波那契Nim
有一堆n个石子,先手可以取1~1-n,之后每个人可1到取前一个人取石子的两倍
可以手算,发现为斐波那契数的时候的P态。
此外,有一个结论,任意一个数都可以分解为一组斐波那契数,这些数在斐波那契数列上不相邻,且分解方式唯一
下棋(SG函数+线段树)
首先计算sg值,根据题目的特点,后继非常多,所以我们用线段树来优化这个过程,建立一个权值线段树。注意sg值会为0所以左边界是0,线段树左端点为0是可以的,树状数组最小值不能为0,要为1。在权值线段树可以找到第一个为0的地方,也就是mex。
对于询问,可以理解为一个新点,向图上连了很多边,于是就要计算新的sg值。所以就是区间mex的问题,可以用主席树高效解决。
#include <bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) (k << 1 | 1)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
const int mx = 1e5 + 5;
vector<int> g[N], topo;
int t[N << 2], sg[N], n, m, k;
void up(int k)
{
t[k] = (t[l(k)] > 0) && (t[r(k)] > 0);
}
void add(int k, int l, int r, int x, int p)
{
if(l == r)
{
t[k] += p;
return;
}
int m = l + r >> 1;
if(x <= m) add(l(k), l, m, x, p);
else add(r(k), m + 1, r, x, p);
up(k);
}
int find(int k, int l, int r)
{
if(l == r) return l;
int m = l + r >> 1;
if(!t[l(k)]) return find(l(k), l, m);
return find(r(k), m + 1, r);
}
//主席树
int s[N << 5], root[N << 5], ls[N << 5], rs[N << 5], cnt;
void build(int& k, int pre, int l, int r, int x)
{
k = ++cnt;
ls[k] = ls[pre]; rs[k] = rs[pre]; s[k] = s[pre] + 1;
if(l == r) return;
int m = l + r >> 1;
if(x <= m) build(ls[k], ls[pre], l, m, x);
else build(rs[k], rs[pre], m + 1, r, x);
}
int ask(int k, int pre, int l, int r)
{
if(l == r) return l;
int m = l + r >> 1, x = s[ls[k]] - s[ls[pre]];
if((m - l + 1) > x) return ask(ls[k], ls[pre], l, m);
return ask(rs[k], rs[pre], m + 1, r);
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
while(m--)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
}
_for(u, 1, n)
{
for(int v: g[u]) add(1, 0, mx, sg[v], -1);
sg[u] = find(1, 0, mx);
add(1, 0, mx, sg[u], 1);
for(int v: g[u]) add(1, 0, mx, sg[v], 1);
}
int ans = 0;
_for(i, 1, n) build(root[i], root[i - 1], 0, mx, sg[i]);
while(k--)
{
int l, r; scanf("%d%d", &l, &r);
ans ^= ask(root[r], root[l - 1], 0, mx);
}
puts(ans ? "Alice" : "Bob");
return 0;
}
A New Tetris Game(SG函数+dfs)
做法非常明显了,暴力dfs竟然能过……不过其实数据范围也只有几十
注意要记忆化一下,一个棋盘可以拉成一行变成stirng,这样就可以用map来实现记忆化。
寻找是否存在时不用0,用find函数。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
int n, m, num, ans;
unordered_map<string, int> mp;
int id(int i, int j)
{
return i * m + j;
}
int dfs(string s)
{
if(mp.find(s) != mp.end()) return mp[s];
unordered_map<int, bool> vis;
rep(i, 0, n - 1)
rep(j, 0, m - 1)
{
if(s[id(i, j)] == '1' || s[id(i + 1, j)] == '1' || s[id(i, j + 1)] == '1' || s[id(i + 1, j + 1)] == '1') continue;
string t = s;
t[id(i, j)] = t[id(i + 1, j)] = t[id(i, j + 1)] = t[id(i + 1, j + 1)] = '1';
vis[dfs(t)] = 1;
}
int res = 0;
while(vis[res]) res++;
return mp[s] = res;
}
int main()
{
scanf("%d", &num);
while(num--)
{
string s = "", x;
scanf("%d%d", &n, &m);
_for(i, 1, n)
{
cin >> x;
s += x;
}
ans ^= dfs(s);
}
puts(ans ? "Yes" : "No");
return 0;
}
P8369 [POI2000]条纹(暴力计算SG函数)
SG函数很好用。简单题直接暴力计算,有些题需要一些数据结构来优化计算
这题直接暴力
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
unordered_map<int, bool> vis;
int sg[N];
int main()
{
int a[5];
_for(i, 1, 3) scanf("%d", &a[i]);
_for(p, 1, 1000)
{
vis.clear();
_for(id, 1, 3)
{
int k = a[id];
_for(i, 0, p - k) vis[sg[i] ^ sg[p - k - i]] = 1;
}
int t = 0;
while(vis[t]) t++;
sg[p] = t;
}
int q; scanf("%d", &q);
while(q--)
{
int x; scanf("%d", &x);
// puts("!@#");
puts(sg[x] ? "1" : "2");
}
return 0;
}
巧合力棒(Nim拓展)
就是在Nim基础上,多了一个选择石子堆的操作
其实从样例可以反推,如果存在异或和为0且剩余石子堆不存在异或和为0的子集则先手必胜
因为先手取出来后,异或和变为0,后手无论是操作还是取出新的石子堆,异或和肯定不是0,然后先手又使其异或和为0,后手同样使得异或和不为0.
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 20;
int a[N], n;
int main()
{
_for(_, 1, 10)
{
scanf("%d", &n);
rep(i, 0, n) scanf("%d", &a[i]);
int ans = 1;
rep(S, 1, 1 << n)
{
int cur = 0;
rep(j, 0, n)
if(S & (1 << j))
cur ^= a[j];
if(!cur) ans = 0;
}
puts(ans ? "YES" : "NO");
}
return 0;
}
取石子(优化状态+记忆化搜索)
这题在不考虑1的情况下很容易
不考虑1的话,操作次数是固定的,合并n-1次,减少为石子总数,加起来就是操作总数,看操作总数奇偶即可。
但是有1的话,取了同时使得合并次数-1,就很难搞。
所以我们分开来看,一堆是非1的,一堆是1的
而对于非1的,可以直接用操作总数来代表它们所有
所以这样就可以设计出一个状态,sg[a][b]表示有a堆1的,非1堆有b的操作次数。
那么就用记忆化搜索计算sg函数即可。
有必胜策略的那一方在操作时会保证在操作非1堆时,让另一方没有方法拿掉一个1的石子堆。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
int sg[N][N * 50], n;
int dfs(int a, int b)
{
if(sg[a][b] != -1) return sg[a][b];
if(b == 1) return dfs(a + 1, 0);
if(a == 0) return sg[a][b] = b % 2;
if(b && !dfs(a, b - 1)) return sg[a][b] = 1;
if(!dfs(a - 1, b)) return sg[a][b] = 1;
if(b && !dfs(a - 1, b + 1)) return sg[a][b] = 1;
if(a > 1 && !dfs(a - 2, b + 2 + (b > 0))) return sg[a][b] = 1;
return sg[a][b] = 0;
}
int main()
{
memset(sg, -1, sizeof sg);
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
int a = 0, b = 0;
_for(i, 1, n)
{
int x; scanf("%d", &x);
if(x == 1) a++;
else b += x + 1;
}
if(b) b--;
puts(dfs(a, b) ? "YES" : "NO");
}
return 0;
}
[CQOI2013] 新Nim游戏(nim+线性基)
这道题nim只是提供一个媒介,主要是线性基
等价于删除石子总数最小的堆,使得剩下的堆不存在子集的异或和为0
考虑如何求一个集合,它的子集异或和不为0。
注意异或和为0意味着可以分割成两个集合异或和相等。
因此用线性基实现,遍历一遍,如果当前数可以由前面的数表示,那么就可以异或和为0,所以此时这个数不能加入。
为了使得这个集合的和最大,先从大到小排序,然后这样子加入即可。
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int M = 40;
const int N = 110;
int a[N], d[M], n;
void add(int x)
{
for(int i = 30; i >= 0; i--)
if(x & (1 << i))
{
if(d[i]) x ^= d[i];
else { d[i] = x; return; }
}
}
int check(int x)
{
for(int i = 30; i >= 0; i--)
if(x & (1 << i))
{
if(d[i]) x ^= d[i];
else return false;
}
return true;
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]);
sort(a + 1, a + n + 1, greater<int>());
ll ans = 0;
_for(i, 1, n)
{
if(check(a[i])) ans += a[i];
else add(a[i]);
}
printf("%lld\n", ans);
return 0;
}
焦糖布丁(树上阶梯Nim+线性基)
这道题简直是前面两道题拼凑起来,秒切
首先在这个树上,石子数往上移,就是阶梯nim,根节点深度为0,那么只有奇数深度的石子是有用的,把它们全部异或起来即可。
因此可以发现只要存在子集异或和为0即可,那么这个可以由线性基来做。
注意开long long
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 70;
ll d[N];
int n;
void add(ll x)
{
for(int i = 62; i >= 0; i--)
if(x & (1LL << i))
{
if(d[i]) x ^= d[i];
else { d[i] = x; return; }
}
}
int check(ll x)
{
for(int i = 62; i >= 0; i--)
if(x & (1LL << i))
{
if(d[i]) x ^= d[i];
else return false;
}
return true;
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
int ans = 0;
memset(d, 0, sizeof d);
scanf("%d", &n);
_for(i, 1, n)
{
ll x; scanf("%lld", &x);
if(check(x)) ans = 1;
else add(x);
}
puts(ans ? "Yes" : "No");
}
return 0;
}
树链博弈(模仿策略)
这题的核心是模仿策略。
先手将哪些层的祖先翻转了,后手就翻哪些。
这样子的话,当前层的黑色节点数少二,其他层的黑色节点数不变。
那么如果每一层黑色节点数都是偶数的话,后手赢,P态。
如果不是,先手可以一步操作达到这个P态,即N态。
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
vector<int> g[N];
int a[N], d[N], n;
void dfs(int u, int fa, int dep)
{
if(a[u]) d[dep]++;
for(int v: g[u])
{
if(v == fa) continue;
dfs(v, u, dep + 1);
}
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]);
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0, 1);
int flag = 1;
_for(i, 1, n)
if(d[i] % 2 == 1)
{
flag = 0;
break;
}
puts(flag ? "Second" : "First");
return 0;
}
AT2307 [AGC010F] Tree Game(分析方法)
首先数据范围暗示是n方的算法,由于要判断每个点,所以可以想到以每个点为根
然后从最简单的情况分析必胜策略,一点点拓展情况,得到一个综合的策略,适合于所有情况的策略。关键的是从最简单的开始分析起,找规律猜结论
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 3e3 + 10;
vector<int> g[N];
int sg[N], a[N], n;
void dfs(int u, int fa)
{
sg[u] = 0;
for(int v: g[u])
{
if(v == fa) continue;
dfs(v, u);
if(!sg[v] && a[u] > a[v]) sg[u] = 1;
}
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]);
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
_for(i, 1, n)
{
dfs(i, 0);
if(sg[i]) printf("%d ", i);
}
return 0;
}
游戏(拓展Nim+第一步方案)
算了一下暴力sg不会T,因此就暴力算sg
在第一步方案上,注意对于sg值,比其小的的sg值都可以达到,大于它的也有部分达到,所以要枚举所有情况。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
unordered_map<int, int> vis;
int sg[N], a[N], n;
void init()
{
_for(i, 1, 1e5)
{
vis.clear();
for(int j = 1; j * j <= i; j++)
if(i % j == 0)
vis[sg[i - j]] = vis[sg[i - i / j]] = 1;
int t = 0;
while(vis[t]) t++;
sg[i] = t;
}
}
int main()
{
init();
int x = 0;
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]), x ^= sg[a[i]];
if(!x)
{
puts("0");
return 0;
}
int cnt = 0;
_for(i, 1, n)
for(int j = 1; j * j <= a[i]; j++)
if(a[i] % j == 0)
{
if(sg[a[i] - j] == (x ^ sg[a[i]])) cnt++;
if(j * j != a[i] && sg[a[i] - a[i] / j] == (x ^ sg[a[i]])) cnt++;
}
printf("%d\n", cnt);
return 0;
}
[SDOI2011]黑白棋(k-nim + dp)
这题是k-nim,也就是说一次可以操作1~k堆石子
结论是将每个数变成二进制表示,ri为第i位二进制1的个数模(k+1)
如果所有所有ri都为0,那么先手必败。
知道这个后,用dp求方案数,这里结合了组合数
因为为0好计算,所以算为0的方案数,最后用总数减去即可
dp[i][j]表示1~i位都是r为0,当前有j个石子的方案数。那么枚举第i+1位的1有x*(d+1)个即可
最后统计答案的适合还要枚举堆的位置,n减去石子数减去终点,在剩下的位置里面选k/2个起点。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 1e4 + 10;
const int mod = 1e9 + 7;
ll dp[20][N], C[N][110];
int main()
{
int n, k, d;
scanf("%d%d%d", &n, &k, &d);
_for(i, 0, 1e4) C[i][0] = 1;
_for(i, 1, 1e4)
_for(j, 1, 100)
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
dp[0][0] = 1;
_for(i, 0, 13)
_for(j, 0, n - k)
for(int x = 0; j + x * (d + 1) * (1 << i) <= n - k && x * (d + 1) <= k / 2; x++)
dp[i + 1][j + x * (d + 1) * (1 << i)] = (dp[i + 1][j + x * (d + 1) * (1 << i)] + dp[i][j] * C[k / 2][x * (d + 1)] % mod) % mod;
ll ans = 0;
_for(j, 0, n - k) ans = (ans + dp[14][j] * C[n - k / 2 - j][k / 2] % mod) % mod;
printf("%lld\n", (C[n][k] - ans + mod) % mod);
return 0;
}
筱玛爱游戏(线性基+博弈)
选一些集合不存在一些数异或和为0,这就是线性基的基
线性基的个数是一定的,所以操作数是一定的,那么就看操作数的奇偶即可。线性基注意long long
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 70;
ll d[N];
int n;
void add(ll x)
{
for(int i = 62; i >= 0; i--)
if(x & (1LL << i))
{
if(d[i]) x ^= d[i];
else { d[i] = x; return; }
}
}
int check(ll x)
{
for(int i = 62; i >= 0; i--)
if(x & (1LL << i))
{
if(d[i]) x ^= d[i];
else return false;
}
return true;
}
int main()
{
int cnt = 0;
scanf("%d", &n);
_for(i, 1, n)
{
ll x; scanf("%lld", &x);
if(!check(x))
{
cnt++;
add(x);
}
}
puts((cnt % 2 == 1) ? "First" : "Second");
return 0;
}
魔法珠(暴击计算SG)
发现暴力计算sg不会T,然后就过了
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
int sg[N], n;
unordered_map<int, int> vis;
int main()
{
sg[1] = 0;
_for(i, 2, 1e3)
{
vector<int> ve;
for(int j = 1; j * j <= i; j++)
if(i % j == 0)
{
ve.push_back(j);
if(j * j != i && j != 1) ve.push_back(i / j);
}
vis.clear();
int cur = 0, t = 0;
for(int x: ve) cur ^= sg[x];
for(int x: ve) vis[cur ^ sg[x]] = 1;
while(vis[t]) t++;
sg[i] = t;
}
while(~scanf("%d", &n))
{
int cur = 0;
_for(i, 1, n)
{
int x; scanf("%d", &x);
cur ^= sg[x];
}
puts(cur ? "freda" : "rainbow");
}
return 0;
}