前言
算法基础课:
第四章 数学知识(四)。
共5题,知识点如下。
容斥原理:
AcWing 890. 能被整除的数。
博弈论:
AcWing 891. Nim游戏、
AcWing 892. 台阶-Nim游戏、
AcWing 893. 集合-Nim游戏、
AcWing 894. 拆分-Nim游戏。
容斥原理
其实跟求面积区别不大
|S|
表示集合元素个数
里面一共有2n -1项,所以时间复杂度为2n
右侧2n 表示每个数都有选或不选的情况,n个数,即有2n个方案
第k个的
AcWing 890. 能被整除的数
算每一个集合的时间复杂度为O(k)
p能被整除和不能被整除时,1-n 中 p 的倍数分别对应的个数
把 i 看成 n 位的二进制数,把 1 - 2n-1 的所有数都看成二进制数
【二进制数,1为选中,0为未选】
就可以用 n 位二进制数来表示各种选法
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 20;
int n, m;
int p[N];
int main() {
// 读入n和m个质数
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> p[i];
}
int res = 0;
for (int i = 1; i < 1 << m; i++) { // 1 << m, 即2的m次方
int t = 1, cnt = 0; // t:当前所有质数的乘积。cnt:当前i包含几个1【当前选法有几个集合】
for (int j = 0; j < m; j++) {
if (i >> j & 1) { // 当前位是1
cnt++;
if ((LL)t * p[j] > n) { // p很大,容易超出范围
t = -1;
break;
}
t *= p[j];
}
}
// 每轮乘积都会算进去即
if (t != -1) { // 没超过n,就需要奇数集合,减去偶数集合
if (cnt % 2) res += n / t;
else res -= n / t;
}
}
cout << res << endl;
return 0;
}
算法相关的数学问题基本都是离散数学,有时间去补补
想提高敲代码的速度的话,敲一道题目,多敲几遍,敲个五六遍就快了,
熟能生巧
简单博弈论
模版等
NIM游戏 —— 模板题 AcWing 891. Nim游戏
公平组合游戏ICG
有向图游戏
Mex运算
SG函数
有向图游戏的和 —— 模板题 AcWing 893. 集合-Nim游戏
定理
题目
模板题 AcWing 891. Nim游戏
思路:
异或,相同为0,不同为1
此时的式子是下一步状态为0或者不为0
1、已经全部为0了,异或上也为0,则必败了
2、当前状态为异或上所有数。值为x,拿走一颗石子,
可以将其转化到等于0的状态
ai ^ x一定小于ai,如下
取走 ai - ai ^ x 后,则ai位置变为ai^x
a1异或到an值为x,则x^x = 0,即下一步一定为0,即必胜
3、当前状态为异或上全部值为0,则不管怎么拿,下一步一定不为0
如果当前是②状态,则拿到最后,后手一定为0,即先手必胜
如果当前是③状态,则拿到最后,先手一定为0,即先手必败
原因:②状态拿完后一定变为③状态,③拿完也一定会变成②状态
先手必胜状态:可以走到某一个必败状态【对手必败】
先手必败状态:走不到任何一个必败状态【对手必胜】
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n;
int res = 0;
scanf("%d", &n);
while (n--) {
int x;
scanf("%d", &x);
res ^= x;
}
if (res) puts("Yes");
else puts("No");
}
AcWing 892. 台阶-Nim游戏
样例
3
2 1 3
Yes
思路:
先手必胜,保持拿完后,台阶1和3永远相等,让对手优先看到0-0,对手必败
一般情况
所有奇数台阶异或结果x不为0,则必胜
可以把奇数台阶上的石子看成是一个经典nim游戏
原因:不为0的时候,经典nim游戏中证明过,一定有一种方式可以拿完石子后,剩下所有奇数异或结果x为0
抛给对手后,x就等于0了
如果对手拿偶数台阶放到奇数,则顺次将其移到下一个偶数台阶,操作完后奇数台阶石子是不变的,则对手局面x依然是0
如果对手拿奇数,那么拿完后x一定不是0,我依然可将其弄为x为0的局面给对手
使对手面对的奇数台阶上永远是0,则对手必败
如果在我这时x=0,则对手也可以让我永远面对x=0的情况,即必败
有的事一开始就注定了,知道结果了,无法改变,就是这样
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n;
int res = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++) { // 注意,台阶从1开始
int x;
scanf("%d", &x);
if (i % 2) res ^= x; // 所有奇数结果异或和
}
if (res) puts("Yes");
else puts("No");
}
AcWing 893. 集合-Nim游戏
mex(S):当前状态不能到的,最小的自然数是多少
如图
能到0能到1就是2,
只能到0就是1,
能到1不能到0就是0
能到0能到1能到2就是3
如果当前
SG(x) = 0
,则为必败
SG(x) != 0
,则为必胜
SG(x) != 0
时。x一定可以到0
任何一个非0的状态都可以到0
先手为非0的话则一定可以保证后手为0
后手不管怎么走一定变成非0
即先手可以保证自己永远是非0,对手永远是0
有n个图可以选择,证明方式与Nim游戏的一样
可以通过把每个有向图的起点值求出来并且异或,判断是否为0
如果是0说明必败,是1必胜
可以通过这样的方式,求出来每一堆石子表示的状态图的SG的值,然后把他们异或起来,如果是0就必败,不是0就必胜
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110, M = 10010;
int n, m;
int s[N], f[M]; // s表示sg的个数,f表示sg的值
// 用记忆化搜索来做
int sg(int x) {
// 用数组来存一下表示某个状态是否被算过,被算过就直接返回
// 保证只被搜索一次,时间为100*10000 = 1e6
if (f[x] != -1) return f[x];
// 用哈希表来存所有可以到的局面
unordered_set<int> S;
for (int i = 0; i < m; i++) {
int sum = s[i];
// 当前数的个数大于sum才可以取石子
// 存入单个图内每个点的sg值
if (x >= sum) S.insert(sg(x - sum));
}
// 判断下集合中不存在的最小自然数是多少
for (int i = 0; ; i++) {
if (!S.count(i)) {
return f[x] = i;
}
}
}
int main() {
// 读入k个数和k个可进行的操作
cin >> m;
for (int i =0 ; i < m; i++) cin >> s[i];
// 读入每一堆的石子个数
cin >> n;
memset(f, -1, sizeof f);
// 将每堆石子的sg值进行异或
int res = 0;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
AcWing 894. 拆分-Nim游戏
一般求SG函数使用的是记忆化搜索,即dp
定理本身不好理解,但用起来特别简单
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110;
int f[N];
int sg(int x) {
if (f[x] != -1) return f[x]; // 如果f[x]算过了,直接返回
unordered_set<int> S; // 存储当前每个局面拆分后能到的局面
for (int i = 0; i < x; i ++) // 一堆石子最多不能超过原来的
for (int j = 0; j <= i; j++) // 另一堆石子最多跟第一堆一样[避免重复];其实 j < x也可以
S.insert(sg(i) ^ sg(j));// 算出每堆石子拆分后的sg值
// mex操作
// 找到集合当中不存在的一个最小的自然数
for (int i = 0; ; i++)
if (!S.count(i))
return f[x] = i; // 不存在,就返回i
}
int main() {
int n;
scanf("%d", &n); // cin >> n;
memset(f, -1, sizeof f); // 所有sg值都是大于0的,用-1表示没算过
int res = 0;
for (int i = 0; i < n; i++) {
int x;
scanf("%d", &x); // cin >> x;
res ^= sg(x); // res^当前石子的sg值
}
if (res) puts("Yes"); // 整个局面的sg值不是0,先手必胜
else puts("No");
}