WHUT第九周训练整理

WHUT第九周训练整理

写在前面的话:我的能力也有限,错误是在所难免的!因此如发现错误还请指出一同学习!

本次题解感谢ljw提供的最后四题题解,本人实在顶不住放弃了!

索引

(难度由题目自身难度与本周做题情况进行分类,仅供新生参考!)

零、基础知识过关

一、easy:02、03、04、05、06、08、13、14、15、16

二、medium:01、07、09、10、12、17、18、21

三、hard:11、19、20、22、23、24、25

本题解报告大部分使用的是C++语言,在必要的地方使用C语言解释。

零、基础知识过关

博弈论不是我的专长,所以写这篇博客也算是大家一起学习了…

对于我来说,提到博弈论那就要与各种类型的组合游戏以及 S G SG​ 函数挂钩了。

1. 什么是组合游戏?

在acm中组合游戏一般有以下特点:

  • 两人进行博弈
  • 两者轮流进行有效操作
  • 有效操作只取决于当前的局面
  • 无法进行有效操作时,当前者败

2. 常见的博弈类型

巴什博奕(Bash)、威佐夫博弈(Wythoff)、斐波那契博弈(Fibonacci)、尼姆博弈(Nim)、公平组合博弈(Impartial Combinatori Games)等等…

参考 https://blog.csdn.net/lgdblue/article/details/15809893

3. 组合游戏术语

  • P-positon:必败态
  • N-positon:必胜态

没有有效操作的局面为 P-position,可以转移到 P-position 的局面是 N-position,所有转移都导致形成 N-position 的局面是 P-position。

4. 引入SG函数

由于各种类型的游戏以及各种游戏的变种与组合,导致不是所有博弈都是明显的,此时引入一个有效的 NP 状态分析工具 S G SG 函数

首先需要定义一个 m e x mex​ 运算, m e x ( S ) mex(S)​ 表示最小是的不属于集合 S S​ 的非负整数,比如: m e x ( { 0 , 1 , 2 , 4 } ) = 3   ,   m e x ( { 2 , 5 } ) = 0   ,   m e x ( ) = 0 mex(\{0, 1, 2, 4\}) = 3~,~mex(\{2, 5\}) = 0~,~mex(\varnothing) =0​

接下来就定义 s g ( x ) = m e x { s g ( y ) y x } sg(x)=mex\{ sg(y) | y是x的后继 \} ,即当前局面的 s g sg 值等于其所有子局面 s g sg 值的 m e x mex

显然,当没有子局面的时候 s g ( x ) = 0 sg(x) = 0 ,此时是 P-position,必败态。那么我们就可以进一步推出当 s g ( x ) = 0 sg(x)=0 时先手必败,后手必胜。所以我们就可以利用 s g sg 函数判断当前局面先手是否必胜。

一般来说, s g sg 函数的时间复杂度是线性的,但并不是所有题目都可以通过 s g sg 函数来直接求解,但是可以用 s g sg 来分析状态来找规律!

参考 https://blog.csdn.net/bestsort/article/details/88197959

5. SG定理

任何的博弈游戏都可以抽象成一张有向无环图,即 D A G DAG​ ,当前局面可以向子局面进行连线,显然 “叶子” 结点是必败态,是 P 点,可以转移到 P 点的是 N 点,只能转移到 N 点的是 P 点,那么这样我们就可以知道图上所有点的 NP 状态。

而博弈游戏进行抽象后可能只有一个 D A G DAG ,也可能有多个 D A G DAG​ ,比如单堆石子与多堆石子的博弈游戏。对于有多个子游戏的游戏怎么处理呢?

这里有一个重要的 S G SG 定理:游戏和的 S G SG 函数等于各个游戏 s g sg 值的 Nim 和(即各个 s g sg 值进行异或的结果)。这样就可以把一个复杂的博弈问题转换成多个子问题来解决,降低了解题的难题。

6. 个人见解

看了很多的博客以及其他资料,我感觉 s g sg 函数跟 d f s dfs 在一定程度上是类似的,在理论上 d f s dfs 是万能解,而 s g sg 函数在理论上可以是博弈问题中的万能解,但是哪有这么好的事情,当状态数很多的时候 d f s dfs s g sg​ 函数的效率就不够了!但是我们仍然可以利用这两者进行打表,找到规律解题!

一、easy

1002:Euclid’s Game(找规律)

题意:给定两个数字 a a​ b b​ S t a n Stan​ O l l i e Ollie​ 轮流使用大的数字减去小的数字的整数倍,问 S t a n Stan​ 先手能否先把其中一个数先变成 0 0​

范围: a a​ b b​ 是整数。

分析:思路很简单,假设当前状态为 ( a , b ) (a, b)​ ,满足 a b a \ge b​

b = = 0 b == 0 或者 a % b = = 0 a\%b == 0 时必胜。

考虑到 a a 可以减去 b b 的整数倍,但是不能减成负数,而已知每个状态要么必胜要么必败。

如果 a a 可以减去多次 b b ,即 a 2 b a \ge 2b​ ,那么就相当于给了我们一个缓冲的空间,我们一定能够达到必胜态。

因为我们可以控制是自己还是对手到达 ( a % b , b ) (a\%b, b) 这个状态,如果 ( a % b , b ) (a\%b, b)​ 是必败态,就让对手到达,否则就自己到达。

Code

#include <bits/stdc++.h>
using namespace std;

void solve(int a, int b, int now)
{
    if (a < b)
        swap(a, b);
    if (b == 0 || a >= b * 2 || a % b == 0)
    {
        cout << (now == 0 ? "Stan wins" : "Ollie wins") << endl;
        return;
    }
    solve(a - b, b, !now);
}

int main()
{
    int a, b;
    while (cin >> a >> b, a + b)
    {
        solve(a, b, 0);
    }
    return 0;
}

1003:Play a game(找规律)

题意:给一个 N N N*N 的棋盘,棋子从角落开始 8600 8600 a i l y a n l u ailyanlu 轮流移动到水平或垂直的未访问格子里,无法移动时当前者败,问 8600 8600 先手是否有必胜策略。

范围: 1 N 1 e 4 1 \le N \le 1e4

分析:就一个参数 N N ,其实瞎猜规律也能过了…

但是写题解还是要证明一下的。

考虑一个 N N 为偶数的正方形,它可以被若干个 1 2 1*2 的长方形完全覆盖,这样先手只要每次走长方形的另一边,保证可以走,而后手必须去寻找新的长方形,必败;否则先手需要去找长方形,必败。

参考 https://www.cnblogs.com/kuangbin/archive/2013/07/22/3204654.html

Code

#include <bits/stdc++.h>
using namespace std;

int n;

int main()
{
    while (cin >> n, n)
    {
        if (n % 2)
        {
            cout << "ailyanlu" << endl;
        }
        else
        {
            cout << "8600" << endl;
        }
    }
    return 0;
}

1004:Brave Game(巴什博奕)

题意:单堆石子,一共有 n n​ 个,两个人轮流取走 1 m 1\sim m​ 个石子,问先手是否有必胜策略,先取光所有石子。

范围: 1 n , m 1000 1 \le n, m \le 1000

分析:经典的巴什博奕背景。

当剩下的石子数量为 m + 1 m+1​ 时必败,要把这样的局面留给对面。

当剩下石子数量为 m + 1 m+1 的倍数时同样也是必败,因为不论我们取多少石子,对面都可以把局面变成下一个 m + 1 m+1 的倍数状态。

因此只需要判断起手时石子数量 n n​ 是否正好是 m + 1 m+1​ 的倍数,如果是,那么无论怎么取都不能把 m + 1 m+1​ 的倍数个石子留给对面,如果不是,则必胜。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        int n, m;
        cin >> n >> m;
        if (n % (m + 1) == 0)
        {
            cout << "second" << endl;
        }
        else
        {
            cout << "first" << endl;
        }
    }
    return 0;
}

1005:Good Luck in CET-4 Everybody!(找规律)

题意:有 n n​ 张牌, K i k i Kiki​ C i c i Cici​ 轮流取走 2 2​ 的幂次数量的牌,问 K i k i Kiki​ 先手是否有必胜策略先取完所有牌。

范围: 1 n 1000 1 \le n \le 1000

分析:发现当数量为 3 3 的时候,对面不论怎么取都是输,当数量是 3 3 的倍数的时候,在对面取完之后我们可以取走数量为 1 1 或者 2 2 的牌来重新构造成 3 3 的倍数局面给对面,直至胜利。

因此只需要判断开局时是否面对的是 3 3 的就可以判断胜负。

Code

#include <bits/stdc++.h>
using namespace std;

int n;

int main()
{
    while (cin >> n)
    {
        if (n % 3 == 0)
        {
            cout << "Cici" << endl;
        }
        else
        {
            cout << "Kiki" << endl;
        }
    }
    return 0;
}

1006:kiki’s game(找规律)

题意:有 N M N*M​ 的矩阵,有一个硬币放在右上角, K i k i Kiki​ Z Z ZZ​ 轮流当硬币移动到左边的格子、下边的格子或者左下的格子,无法移动时当前者败,问 K i k i Kiki​ 先手是否有必胜策略。

范围: 1 N , M 2000 1 \le N,M \le 2000​

分析:画画图规律就很明显了,当行数和列数均为奇数时必败,否则必胜!

在这里插入图片描述
本题唯一的问题就是用 C++ 提交不管咋样都是显示 MLE,最后只能上 java 了… 下面也附了 java 的代码。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n, m;
    while (cin >> n >> m, n + m)
    {
        if (n % 2 && m % 2)
        {
            cout << "What a pity!" << endl;
        }
        else
        {
            cout << "Wonderful!" << endl;
        }
    }
    return 0;
}

// import java.util.Scanner;

// public class Main {
	
// 	public static void main(String[] args) {
// 		Scanner in = new Scanner(System.in);
// 		int a, b;
// 		while(in.hasNextInt()) {
// 			a = in.nextInt();
// 			b = in.nextInt();
// 			if(a == 0 && b == 0) break;
// 			if(a%2 == 1 && b%2 == 1) {
// 				System.out.println("What a pity!");
// 			}
// 			else {
// 				System.out.println("Wonderful!");
// 			}
// 		}
// 	}
// }

1008:邂逅明下(巴什博奕)

题意:有 n n 个硬币,两个人轮流取走 p q p \sim q​ 个硬币,取完者胜,问先手是否有必胜策略。

范围: n , p , q 65536 n, p, q \le 65536

分析:当 n q n \le q 时必胜,那么显然当 q < n p + q q < n \le p+q 时必败。

可以继续推得以 p + q p+q 为一个周期,先出现 q q 个必胜态,再出现 p p 个必败态,那么只要判断 n % ( p + q ) n\% (p+q) 的结果是否是必胜态即可。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n, p, q;
    while (cin >> n >> p >> q)
    {
        if (n % (p + q) <= p && n % (p + q))
        {
            cout << "LOST" << endl;
        }
        else
        {
            cout << "WIN" << endl;
        }
    }
    return 0;
}

1013:No Gambling(找规律)

题意:给定 N N ,表示蓝方的棋盘为 N ( N + 1 ) N*(N+1) ,红方的棋盘为 ( N + 1 ) N (N+1)*N ,下面给出的是 N = 4 N=4 的情况。双方轮流选择属于自己颜色的两个相邻的点进行连线。蓝方的目标是要从左边连到右边,红方的目标是从上方连到下方,问蓝方先手谁能赢。
在这里插入图片描述

范围: 2 N 270000 2 \le N \le 270000

分析:这题就不多说了,随便画画你会发现怎么都输不了… 因此先手必赢。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n;
    while (cin >> n)
    {
        if (n == -1)
            break;
        cout << "I bet on Oregon Maple~" << endl;
    }
    return 0;
}

1014:Coin Game(找规律)

题意:有 N N 个围成一圈的硬币,两个人轮流取走 1 k 1 \sim k 个连续的硬币(取走就不形成圈了!)。问先手是否能够先取完所有硬币。

范围: 3 N 1 e 9   ,   1 k 10 3 \le N \le 1e9~,~1 \le k \le 10​

分析:还以为取完硬币之后还是圈,导致错了好几发…

如果一次性就可以取完,那么先手必胜,否则后手必定可以将当前的硬币分成长度相同的两段,那么接下来先手只能从其中一段进行操作,后手在另一段也进行同样的操作,这样就能保证后手必胜。

而分成长度相同的两段,需要满足 k > 1 k > 1 ,当 k = 1 k = 1​ 时,根据奇偶性就可以判断胜负。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int T;
    cin >> T;
    int kase = 1;
    while (T--)
    {
        int n, k;
        cin >> n >> k;
        cout << "Case " << kase++ << ": ";
        if (n <= k || (k == 1 && n % 2))
        {
            cout << "first" << endl;
        }
        else
        {
            cout << "second" << endl;
        }
    }
    return 0;
}

1015:悼念512汶川大地震遇难同胞——选拔志愿者(巴什博奕)

又是裸的巴什博奕,不说了。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        int n, m;
        cin >> n >> m;
        if (n % (m + 1) == 0)
        {
            cout << "Rabbit" << endl;
        }
        else
        {
            cout << "Grass" << endl;
        }
    }
    return 0;
}

1016:Public Sale(巴什博奕)

题意:拍卖,底价为 0 0 ,两个人轮流开始加价 1 N 1 \sim N ,谁先加价到 M M​ 谁胜,问先手是否有必胜策略,如果有则输出第一次加价能出的价格。

范围: 0 < N , M < 1100 0 < N, M < 1100

分析:还是巴什博奕,只是多了需要输出所有的答案。

可以发现如果第一次就可以全部取完,那么才可能有多种方案,否则只有一种方案,即加价到最近的 N + 1 N+1 的倍数。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int m, n;
    while (cin >> m >> n)
    {
        if (m % (n + 1) == 0)
        {
            cout << "none" << endl;
        }
        else
        {
            if (m <= n)
            {
                int first = 1;
                for (int i = m; i <= n; i++)
                {
                    if (first)
                        first = 0;
                    else
                        cout << " ";
                    cout << i;
                }
                cout << endl;
            }
            else
            {
                cout << m % (n + 1) << endl;
            }
        }
    }
    return 0;
}

二、medium

1001:Calendar Game(找规律)

题意:给定一个日期(年/月/日), A A​ B B​ 轮流操作日期,可以移动到下一天,或者下个月同一天,两者都采取最优策略,问 A A​ 先手是否能够先恰好到达 2001.11.4 2001.11.4​ 这一天。

范围:日期在 1900.1.1 1900.1.1​ 2001.11.4 2001.11.4​ 之间。

分析:通过观察发现如果当前的日期为 ( x , y ) (x, y) ,可以通过一次转移到 ( x , y + 1 ) (x, y+1) ( x + 1 , y ) (x+1, y) ,奇偶性发生了变化,而目标的日期 ( 11 , 4 ) (11, 4) 11 + 4 11+4 为奇数。

因此如果先手时所面对的日期为偶数,那么就可以转成奇数给对方,我们又可以面对偶数,最后取得胜利。

但是还要考虑细节,不是所有的日期经过转换之后奇偶性都会发生改变!

首先,月份 12 + 1 12+1​ 会变成 1 1​ 月,奇偶性发生改变,所以不用处理。

再考虑一般每个月要么 30 30 31 31 天, 31 1 31\rightarrow1 奇偶性发生改变,但是 30 30 号根据不同的月份会到 31 31 或者 1 1​ ,前者的奇偶性改变,后者不一定。

枚举月份发现 9 9 月和 11 11 30 30 号的时候天数 + 1 +1 之后奇偶性不变,因此虽然 ( 9 , 30 ) (9,30) ( 11 , 30 ) (11,30) 是奇数,但是他们一次转移后还是奇数,可以胜利。

最后是闰年的问题,只要考虑 2.29 2.29 这一天,可以转移到 ( 3 , 29 ) (3,29) 或者 ( 3 , 1 ) (3,1) ,两者都是偶数,赢不了。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        int year, month, day;
        cin >> year >> month >> day;
        if ((month + day) % 2 == 0 || (day == 30 && (month == 9 || month == 11)))
        {
            cout << "YES" << endl;
        }
        else
        {
            cout << "NO" << endl;
        }
    }
    return 0;
}

1007:取石子游戏(斐波那契博弈)

题意:单堆石子,一共有 n n​ 个,两个人轮流取石子,每次取石子的数量不能超过上个人的 2 2​ 倍,问先手是否能先取完。

范围: 2 n 2 31 2 \le n \le 2^{31}

分析:经典斐波那契博弈。

根据 Z e c k e n d o r f Zeckendorf 定理:任何正整数可以表示为若干个不连续的 F i b o n a c c i Fibonacci 数之和。

那么我们就可以假设剩余石子数 n = a [ 0 ] + a [ 1 ] + . . . + a [ m ] n = a[0]+a[1]+...+a[m] ,其中 a [ i ] a[i] F i b o n a c c i Fibonacci 数,即把这一堆石子分成 m m 堆石子来做。

可以知道相邻的 a a 值之间不可能是在原斐波那契序列上是连续的,比如 n = 1 + 2 n = 1+2 ,这种情况不会出现,因为 1 + 2 = 3 1+2=3​ ,两个相邻斐波那契数之和是下一个斐波那契数。

那么得到 a [ i ] > 2 a [ i 1 ] a[i] > 2*a[i-1] (假设 a a 是递增的)

因此我们可以从 a [ 0 ] a[0] 开始拿,对面下一次必不可能直接拿完 a [ 1 ] a[1]

那么对于每一堆石子我们都可以拿掉最后一个石子,直到游戏胜利。

因此需要满足至少有两堆石子,即石子数 n n​ 不是斐波那契数。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 50 + 10;

long long fib[MAXN];  // 斐波那契数组

int main()
{
    // 预处理斐波那契,只需要50就够了
	fib[1] = fib[2] = 1;
	for (int i = 3; i <= 50; i++)
	{
		fib[i] = fib[i - 1] + fib[i - 2];
	}
	int n;
	while (cin >> n, n)
	{
		int flag = 0;
        // 判断是不是斐波那契数
		for (int i = 1; i <= 50; i++)
		{
			if (fib[i] == n)
			{
				flag = 1;
				break;
			}
		}
		if (flag)
			cout << "Second win" << endl;
		else
			cout << "First win" << endl;
	}
	return 0;
}

1009:Nim or not Nim?(sg打表找规律)

题意:有 n n 堆石子 S S ,两个人轮流对某一堆石子取走任意数量的石子,或者把这堆石子分成两份,问先手是否有必胜策略先取完所有石子。

范围: 1 n 1 e 6   ,   1 S [ i ] 2 31 1 1 \le n \le 1e6~,~1\le S[i] \le 2^{31}-1

分析:在经典Nim游戏上加入一个新的规则,可以把一堆石子分成两堆,在Nim游戏中,每堆石子的 s g sg 值就是本身的数量,结果只要把所有石子的 s g sg 值进行异或就可以了。这道题目中每堆石子的 s g sg 值发生了改变, s g ( x ) = m e x { s g ( 0 ) , s g ( 1 ) . . . s g ( x 1 ) , s g ( 1 ) s g ( x 1 ) , s g ( 2 ) s g ( x 2 ) . . . } sg(x) = mex\{sg(0),sg(1)...sg(x-1),sg(1)\oplus sg(x-1),sg(2) \oplus sg(x-2)...\} ,其中 \oplus 是异或操作,除了可以对一堆石子取走任意数量变成 0 1 2... 0、1、2... 个石子,也可以分成两堆,两堆的数量可以是 ( 1 , x 1 ) ( 2 , x 2 ) . . . (1, x-1),(2, x-2)... ,分堆要把他们的 s g sg 值异或起来,这同样也是根据 s g sg 定理。

这样我们就可以根据 s g sg​ 来打表,如下:

在这里插入图片描述

i i 表示一堆石头的数量, s g sg 就是本题中单堆 i i 个石子的 s g sg 值。

可以发现当 i % 4 = = 0 i\%4 == 0 时, s g [ i ] = i 1 sg[i] = i-1

i % 4 = = 3 i\%4 == 3 时, s g [ i ] = i + 1 sg[i] = i+1​

其余 s t [ i ] = i st[i] = i

因此我们就根据这样的规律对他们的 s g sg 值进行异或就可以得到答案。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        int n;
        cin >> n;
        int ans = 0;
        for (int i = 0; i < n; i++)
        {
            int x;
            cin >> x;
            if (x % 4 == 3)
                ans ^= x + 1;
            else if (x % 4 == 0)
                ans ^= x - 1;
            else
                ans ^= x;
        }
        if (ans)
            cout << "Alice" << endl;
        else
            cout << "Bob" << endl;
    }
    return 0;
}

// 下面是sg打表程序
// #include <bits/stdc++.h>
// using namespace std;

// const int MAXN = 1000 + 10;

// int sg[MAXN], vis[MAXN];

// int SG(int x)
// {
//     if (x == 0)
//         return 0;
//     if (x == 1)
//         return 1;
//     memset(vis, 0, sizeof(vis));
//     for (int i = 0; i < x; i++)
//     {
//         vis[sg[i]] = 1;
//     }
//     for (int i = 1; i < x; i++)
//     {
//         vis[sg[i] ^ sg[x - i]] = 1;
//     }
//     for (int i = 0; i < MAXN; i++)
//     {
//         if (!vis[i])
//             return i;
//     }
//     return 0;
// }

// int main()
// {
//     sg[0] = 0;
//     for (int i = 1; i < 20; i++)
//     {
//         sg[i] = SG(i);
//         cout << "i: " << i << " sg: " << sg[i] << endl;
//     }
//     return 0;
// }

1010:Game(阶梯博弈)

题意:有 n n 个盒子,第 i i 个盒子里面的卡片数量为 a r r [ i ] arr[i] A l i c e Alice B o b Bob 轮流选择一个非空盒子 A A ,再选择另一个卡片数量更少的盒子 B B ,将 A A 中任意数量的卡牌转移到 B B 中,两个盒子需要满足 ( A + B ) % 2 = 1 (A+B)\%2 = 1 ( A + B ) % 3 = 0 (A+B)\%3 = 0 。无法进行合法操作的人败。问 A l i c e Alice 先手是否有必胜策略。

范围: 1 n 1 e 4   ,   a r r [ i ] 100 1 \le n \le 1e4~,~arr[i] \le 100

分析:这两个条件其实可以合并成一个条件,即 ( A + B ) % 6 = = 3 (A+B)\%6 == 3

那么就可以画出转移图,如下:

img

图片引用于 https://blog.csdn.net/qq_21048401/article/details/48140263

可以发现最后只有 1 , 3 , 4 1,3,4 是没有办法再去转移的,因此是终态。除此以外,发现其他 a r r [ i ] % n = 0 , 2 , 5 arr[i]\%n = 0,2,5 的点都是经过奇数次转移到终态,而其余的都是经过偶数次转移到终态。

这样问题就转换成阶梯Nim的博弈问题了,不懂的同学可以自行百度~

对于偶数次转移的数字 x x ,我们不需要管,它们对最后的答案是没有影响的,因为如果对手转移了 x x 一次,我们可以再转移一次,就形成了另一个偶数次转移的数字 x x' ,直到这个数字到达了终态,而此时剩下的局面就相当于消失了一个数字 x x ,相当于不存在。

那么我们只需要对奇数次转移的数字进行异或就可以了。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int T;
    cin >> T;
    int kase = 1;
    while (T--)
    {
        int n;
        cin >> n;
        int ans = 0;
        for (int i = 1; i <= n; i++)
        {
            int x;
            cin >> x;
            if (i % 6 == 0 || i % 6 == 2 || i % 6 == 5)
                ans ^= x;
        }
        cout << "Case " << kase++ << ": " << (ans ? "Alice" : "Bob") << endl;
    }
    return 0;
}

1012:Alice’s Game(贪心+博弈)

题意:给 N N X i Y i X_i*Y_i 的巧克力, A l i c e Alice 只能竖着切, B o b Bob 只能横着切, 不能切出更小的巧克力者败。问 A l i c e Alice 先手谁会赢。

范围: 1 N 100   ,   1 X i , Y i 1 e 9 1 \le N \le 100~,~1 \le X_i,Y_i \le 1e9

分析:按照一般的思路来说,每一刀切下去应该让对方损失最大,而不能出现切出像 1 5 1*5​ 或者 5 1 5*1​ 这样的巧克力给对方,因为这样对方就可以在上面继续切 4 4​ 刀,而我们只能干看着。所以自然想到应该每次都对半切,这样才会让上面的情况的出现时间最晚!因为都是对半切,所以切出来的巧克力具有对称性,只需要考虑其中一块就行,即双方只对其中一块一直对半切,这样统计 N N​ 块巧克力双方能够切的最多次数,进行比较即可。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int T;
    cin >> T;
    int kase = 1;
    while (T--)
    {
        int n;
        cin >> n;
        long long ans1 = 0, ans2 = 0;  // 注意ll
        for (int i = 0; i < n; i++)
        {
            int x, y;
            cin >> x >> y;
            while (x > 1 && y > 1)  // 保证都可以切
            {
                // 对半切,统计答案
                x /= 2;  
                y /= 2;
                ans1++;
                ans2++;
            }
            // 哪方先没法切,那么对面就可以多切
            if (x == 1)
                ans2 += y - 1;
            if (y == 1)
                ans1 += x - 1;
        }
        // 最后进行比较即可
        if (ans1 <= ans2)
        {
            cout << "Case " << kase++ << ": Bob" << endl;
        }
        else
        {
            cout << "Case " << kase++ << ": Alice" << endl;
        }
    }
    return 0;
}

1017:Being a Good Boy in Spring Festival(尼姆博弈)

题意:有 M M 堆扑克牌,每堆牌的数量为 N i N_i ,两个人轮流选择其中一堆牌取走任意数量( > 1 >1 )的牌,问先手是否有必胜策略,有则输出第一步的方案数。

范围: 1 < M 100   ,   1 N i 1 e 6 1 < M \le 100~,~1 \le N_i \le 1e6​

分析:经典Nim博弈,就是多了一个计算方案数。

N 1 N 2 . . . N M = 0 N_1 \oplus N_2 \oplus ...\oplus N_M = 0 时先手必败;

否则 N 1 N 2 . . . N M = k N_1 \oplus N_2 \oplus ...\oplus N_M = k ,先手必胜。

此时我们需要拿牌使得异或和为 0 0 的局面留给对方,那我们尝试将每堆牌数 N i N_i 变成 N i k N_i \oplus k ,如果结果比 N i N_i​ 小,说明是可行的答案。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;

int arr[MAXN];

int main()
{
    int n;
    while (cin >> n, n)
    {
        int ans = 0;
        for (int i = 0; i < n; i++)
        {
            cin >> arr[i];
            ans ^= arr[i];
        }
        int cnt = 0;
        for (int i = 0; i < n; i++)
        {
            if ((arr[i] ^ ans) < arr[i])
                cnt++;
        }
        if (ans == 0)
            cout << 0 << endl;
        else
            cout << cnt << endl;
    }
    return 0;
}

1018:取(m堆)石子游戏(尼姆博弈)

题意:有 M M​ 堆石子,每堆石子的数量为 N i N_i​ ,两个人轮流选择其中一堆石子取走任意数量( > 1 >1​ )的石子,问先手是否有必胜策略,有则输出所有可行的方案。

范围: M 2 e 5 M \le 2e5 N i N_i 是正整数

分析:跟 1017 1017 差不多,稍微改一两句话即可。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2e5 + 10;

int arr[MAXN];

int main()
{
    int n;
    while (cin >> n, n)
    {
        int ans = 0;
        for (int i = 0; i < n; i++)
        {
            cin >> arr[i];
            ans ^= arr[i];
        }
        if (ans == 0)
        {
            cout << "No" << endl;
            continue;
        }
        cout << "Yes" << endl;
        for (int i = 0; i < n; i++)
        {
            if ((arr[i] ^ ans) < arr[i])
            {
                cout << arr[i] << " " << (arr[i] ^ ans) << endl;
            }
        }
    }
    return 0;
}

1021:A Multiplication Game(找规律)

题意:给定数字 n n ,两个人从 1 1 开始轮流对这个数字乘以 2 9 2 \sim 9 ,问先手能否先得到超过 n n 的数字。

范围: 1 < n < 4294967295 1 < n < 4294967295 ​

分析:打打表,发现跟边界的 2 2 9 9 有关。

n [ 2 , 9 ] n \in [2, 9] S t a n Stan

n [ 10 , 18 ] n \in [10, 18] O l l i e Ollie

n [ 19 , 162 ] n \in [19,162] S t a n Stan

n [ 163 , 324 ] n \in [163,324] O l l i e Ollie​

n [ 325 , 2916 ] n \in [325, 2916] S t a n Stan

左边界 = 上阶段右边界+1;

右边界 = 9 2 9 2... 9*2*9*2...

因此我们只需要判断当前的 n n 处于哪个阶段就可以了。

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    long long n;
    while (cin >> n)
    {
        long long cnt = 0, now = 1;
        while (now < n)
        {
            if (cnt % 2)
                now *= 2;
            else
                now *= 9;
            cnt++;
        }
        if (cnt%2)
            cout << "Stan wins." << endl;
        else
            cout << "Ollie wins." << endl;
    }
    return 0;
}

三、hard

1011:Daizhenyang’s Coin(Mock Turtles 硬币游戏)

题意:有非常多个硬币,其中有 k k 个正面向上,两个人轮流选择一个、二个或三个硬币进行翻转,必须满足翻转的硬币中最右边的那个原先是正面朝上的,无法进行翻转者败。问先手是否有必胜策略。

范围: 0 k i 1 e 8 0 \le k_i \le 1e8

分析:经典的 Mock Turtles 硬币游戏 ,这道题目为什么是 hard 呢,因为我看了网上的题解,都跟 kuangbin 是一样的,而 kuangbin 里面说到 MT 游戏的 s g sg​ 值与方案数是相同的,但是没有解释为什么,如果直接按照当前局面导致的子局面的 s g sg​ 值进行计算的话是算不出来的,因为计算是递归无解的,所以我也不知道怎么打出这个 s g sg​ 表并且找出跟二进制中 1 1​ 的数量的关系:设 c n t cnt​ x x​ 二进制串中 1 1​ 的数量。

s g ( x ) = { 2 x c n t % 2 = 1 2 x + 1 c n t % 2 = 0 sg(x) = \begin{cases} 2*x,cnt\%2 = 1\\ 2*x+1 ,cnt\%2 = 0 \end{cases}​

如果有兴趣的同学可以去看看…说不定你可以看懂!

https://www.cnblogs.com/kuangbin/p/3218060.html

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;

int sg(int x)
{
    int tmp = x;
    int cnt = 0;
    while (tmp)
    {
        if (tmp & 1)
            cnt++;
        tmp /= 2;
    }
    if (cnt & 1)
        return 2 * x;
    else
        return 2 * x + 1;
}

int arr[MAXN];

int main()
{
    int n;
    while (cin >> n)
    {
        for (int i = 0; i < n; i++)
        {
            cin >> arr[i];
        }
        // 注意去重
        sort(arr, arr + n);
        n = unique(arr, arr + n) - arr;
        int sum = 0;
        for (int i = 0; i < n; i++)
        {
            sum ^= sg(arr[i]);
        }
        if (sum)
            cout << "No" << endl;
        else
            cout << "Yes" << endl;
    }
    return 0;
}

1019:取石子游戏(威佐夫博弈)

题意:有两堆石子,数量分别为 a a b b ,两个人轮流从其中一对取走任意数量( > 1 >1 )的石子或者从两堆中同时拿走相同数量的石子,问先手是否有必胜策略先把石子全部取完。

范围: a , b 1 e 9 a, b \le 1e9​

分析:经典的威佐夫博弈,为什么是 h a r d hard​ 呢?因为我看不懂…但是结论还是挺简洁的,有兴趣的同学可以去学习一下!

参考 https://www.cnblogs.com/jackge/archive/2013/04/22/3034968.html

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
	int a, b;
	while (cin >> a >> b)
	{
		if (a < b)
			swap(a, b);
		a = (int)((a - b) * (1 + sqrt(5)) / 2.0);
		if (a == b)
			cout << 0 << endl;
		else
			cout << 1 << endl;
	}
	return 0;
}

1020:取(2堆)石子游戏(威佐夫博弈+枚举)

题意:有两堆石子,数量分别为 a a​ b b​ ,两个人轮流从其中一对取走任意数量( > 1 >1​ )的石子或者从两堆中同时拿走相同数量的石子,问先手是否有必胜策略先把石子全部取完,有则输出所有方案。

范围: a , b 1 e 9 a, b \le 1e9​

分析:上面那题的加强版,要求输出所有方案时,有了结论之后只要暴力枚举就可以了。既然上面裸题都是 h a r d hard​ 了,那这题也只能是 h a r d hard​ 了。

参考 https://www.cnblogs.com/clliff/p/4259746.html

Code

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int a, b;
    while (cin >> a >> b, a + b)
    {
        if (a > b)
            swap(a, b);
        double k = (1 + sqrt(5)) / 2.0;
        if (a == (int)((b - a) * k))
        {
            cout << 0 << endl;
        }
        else
        {
            cout << 1 << endl;
            for (int i = 1; i <= a; i++)
            {
                int x = a - i, y = b - i;
                if ((int)((y - x) * k) == x)
                {
                    cout << x << " " << y << endl;
                }
            }
            for (int i = b - 1; i >= 0; i--)
            {
                int x = a, y = i;
                if (x > y)
                    swap(x, y);
                if ((int)((y - x) * k) == x)
                {
                    cout << x << " " << y << endl;
                }
            }
        }
    }
    return 0;
}

1022:A simple stone game(k倍动态减法游戏)

题意:一堆石子有 n n​ 个,第一个人第一次最多拿 n 1 n-1​ 个,后手最多能拿先手的 k k​ 倍,问先手第一次最少要拿多少个石头能确保获胜,如果不能取胜,则输出 l o s e lose​

范围: 2 n 1 e 8   ,   1 k 1 e 5 2 \le n \le 1e8~,~1 \le k \le 1e5

分析:典型k倍动态减法游戏,具体可以查看这篇论文

简化思路,能拿到最后一个石头就可以取得胜利,思考斐波那契博弈,我们得到的结论是如果是斐波那契数列里的数,则必输,否则必胜。原理不再赘述,在证明斐波那契博弈的时候我们用到的便是将一堆石子分成 x x 堆(所有数字都可以变成斐波那契数列中的数字相加,且分解出的数字绝不相邻),这样我们就可以保证能拿完每一堆的最后一个。同理,该游戏也可以分解为 x x 堆来做,那么问题就转化为如何构造这个数列。

目的:

  1. 构造一个数列使这个数列中的数字能表示所有正整数
  2. 表示正整数的时候相邻的两项必须超过k倍

那么我们可以创建一个数组 a a​ 来存放我们的答案,因为要达成目的,所以还需要另一个 b b​ 数组来保存当前能表示的数字的最大值。

很显然 a [ 0 ] = b [ 0 ] = 1 , a [ i + 1 ] = b [ i ] + 1 a[0]=b[0]=1,a[i + 1] = b[i] + 1​ ,那么要保证第二个目的,我们就需要得到 b [ i ] b[i]​ 的递推式。

假设 a [ j ] k < a [ i ] a[j] * k < a[i] ,那么我们当前能表示的最大数字就可以变为 a [ i ] + b [ j ] a[i] + b[j] ,因为 b [ j ] b[j] 是从 1 a [ j ] 1-a[j] 能表示的最大数字。那么就得到了这样一个式子 b [ i ] = a [ i ] + b [ j ] ( j = m a x ( { j i f a [ j ] k < a [ i ] } ) ) b[i] = a[i] + b[j] (j = max(\{j | if a[j] * k < a[i]\})) ,如果不满足这个条件,那么很显然 b [ i ] = a [ i ] b[i] = a[i]​

根据上述条件,我们就可以写出一个递推函数来进行计算,之后就是和斐波那契博弈一样处理即可。

Code

#include <cstdio>
const int maxn = 10000005;
long long a[maxn], b[maxn];
int main()
{
	int t, n, k;
	while(~scanf("%d", &t)){
		for(int i = 1; i <= t; i++){
			scanf("%d %d", &n, &k);
			a[0] = b[0] = 1;
			int p = 0, q = 0;
			while(a[p] < n){
				a[p + 1] = b[p] + 1;
				p++;
				while(a[q + 1] * k < a[p]){
					q++;
				}
				if(a[q] * k < a[p]){
					b[p] = a[p] + b[q];
				} else {
					b[p] = a[p];
				}
			}
			printf("Case %d: ", i);
			if(n == a[p]){
				puts("lose");
			} else {
				int ans = 0;
				while(n){
					if(n >= a[p]){
						n -= a[p];
						ans = a[p];
					}
					p--;
				}
				printf("%d\n", ans);
			}
		}
	}
	return 0;
}

1023:Climbing the Hill(阶梯博弈)

题意:有 n n 个人爬山,山顶坐标为 0 0 ,其他人 h i h_i 按升序给出,不同的坐标只能容纳一个人(山顶不限), A l i c e Alice B o b Bob 轮流选择一个人让他移动任意步,但不能越过前面的人,且不能和前面一个人在相同的位置。现在有一个人是 k i n g king ,给出 k i n g king 是哪个人( i d id ),谁能将国王移动到山顶谁胜。

范围: 1 n 1000   ,   1 k n   ,   0 < h i < 1 e 5 1 \le n \le 1000~,~1 \le k \le n~,~0 < h_i < 1e5

分析:先考虑简化版,没有king,谁先不能移动谁输掉。和阶梯博弈类似(blog)。根据人数的奇偶性:把人从上顶向下的位置记为 a 1 , a 2 , . . . a n a_1,a_2,...a_n ,如果为偶数个人,则把 a 2 i 1 a_{2i-1} a 2 i a_{2i} 之间的距离-1(空格数)当做一个Nim堆,变成一共 n 2 \frac {n}{2} 堆的Nim游戏;如果为奇数个人,则把山顶到 a 1 a_1 的距离(这是距离)当做一个Nim堆, a i 2 a_{i*2} a i 2 + 1 a_{i*2+1} 的距离-1当做Nim堆,一共 n + 1 2 \frac {n+1}{2} 堆,相当于往后面(山下)移动石子(从山上到山下是一个阶梯,石子向山下传递)。

考虑King的情况和上述版本几乎一致,只要把King当作普通人一样处理即可。除了两种特殊情况:1. 当King是第一个人时,Alice直接胜 2. 当King是第二个人且一共有奇数个人时,第一堆的大小需要减1。

(此处转载blog)

Code

#include<iostream>
#include<cstdlib>
#include<stdio.h>
using namespace std;
int a[1001];
int main()
{
    int i,k,n,t;
    a[0]=-1;
    while(scanf("%d%d",&n,&k)!=EOF){
        for(i=1;i<=n;i++) scanf("%d",&a[i]);
        if(k==1){
            puts("Alice");
            continue;
        }
        int sg=0;
        for(i=n;i>0;i-=2)
            sg^=(t=a[i]-a[i-1]-1);
        if((n&1)&&k==2)
            sg=sg^t^(t-1);
        if(sg) puts("Alice");
        else puts("Bob");
    }
    return 0;
}

1024:A Puzzle for Pirates(海盗分金问题)

题意:有 n n​ 个海盗, m m​ 个金币,按温顺的顺序给海盗编号,使最温顺的是 1 1​ 号,从海盗 n n​ 开始提分配方案,如果有半数及以上海盗同意则不用被扔。从海盗 n n​ 开始提方案,问海盗 p p​ 能分到多少金币。

范围: 1 n 1 e 4   ,   1 m 1 e 7   ,   1 p n 1 \le n \le 1e4~,~1 \le m \le 1e7~,~1 \le p \le n

分析:肯定要分类讨论。

首先观察两个人的情况, 2 2 提出他要全部的, 2 2 自己本身会同意,则能拿到全部的钱。

增加了一个人的时候, 2 2 肯定不会赞同 3 3 的方案,因为如果 3 3 被扔了,那么 2 2 就能拿到所有的金币。所以只需要让 1 1 赞同 3 3 的方案即可。那么 1 1 肯定是 1 1 个金币都拿不到的,所以只需要给 1 1​ 一个金币,就可以了。

再增加一个人的时候,只需要让 2 2 赞同 4 4 的方案,就可以了。那么如果 2 2 不赞同 4 4 的方案,转化成 3 3 个人的情况, 2 2 一个金币都拿不到,那么只需要给 2 2 一个金币即可。

通过总结规律我们可以发现,当金币足够的时候,只需要让和自己同奇偶编号的人支持就行了。

那如果金币不足够的时候,例如有 10 10 个金币, 23 23 个海盗,那么 23 23 不管提出什么样的方案都会被扔。

增加一个海盗的时候,不管他提出怎么样的方案, 23 23 一定会赞同他,因为一旦不赞同,他就会被扔。所以 24 24 会得到一共 12 12​ 个人支持,那么他就能活下来。

继续增加一个海盗,前面海盗不会管这名海盗提的任何要求,所以必死

继续增加, 25 25 必定支持 26 26 提出的方案,那么一共有 12 12 个人支持, 26 26 还是必死。

继续增加, 27 27 会获得 13 13 个人支持,必死

继续增加, 28 28 会获得 14 14 个人支持,能活。

那么得到一个结论,必然会有 m m 个人支持自己,那么还需要至少 n 2 m 1 \lceil \frac {n}{2} \rceil - m - 1 个人支持。那么设能存活的海盗的编号为 a i a_i ,那么在他之后一部分海盗是必死的(如果没有更大的 a i + 1 a_{i+1} 出现),那么我们就可以得到这样一个式子

a i + 1 a i = a i + 1 2 m a_{i+1}-a_{i} = \frac {a_{i+1}}{2} - m​ ,也就是 a i + 1 = 2 a i 2 m a_{i+1} = 2 * a_{i} - 2*m​ 只有满足这个式子,才能保证不死。通过解这个递推方程,我们可以得到一个通项公式 a i = 2 i a 0 m ( 2 i 2 ) a_{i} = 2^i * a_0 - m*(2^i - 2)​ ,又因为 a 0 = 2 m + 1 a_0 = 2 * m + 1​ ,所以 a i = 2 i + 2 m a_i = 2^i + 2*m​ ,那么就得到了结论,如果 p < = a i p <= a_i​ ,则不死,得到 0 0​ 个金币(因为后面的人不一定会给你,所有人都是处于 0 / 1 0/1​ 的状态),否则,必死。

那么总结如下:

  1. 海盗是金币的两倍及以下

    那第一个提出分配方案的必会被半数以上的人通过。所以如果 p = n p=n​ 答案为 m n 1 2 m - \frac{n-1}{2}​ ,其他人如果和最后一个人奇偶性相同,则得到一个金币。

  2. 其他情况

    如果 p < = a i p <= a_i ,则不死,得到 0 0 个金币(因为后面的人不一定会给你,所有人都是处于 0 / 1 0/1​ 的状态),否则,必死。

Code

#include <cstdio>
int main()
{
	int t, n, m, p;
	while(~scanf("%d", &t)){
		while(t--){
			scanf("%d %d %d", &n, &m, &p);
			if(n > 2 * m + 1){
				int tmp = n - (m << 1), tp = 1;
				while(tp <= tmp){
					tp <<= 1;
				}
				tp >>= 1;
				if(2 * m + tp >= p){
					puts("0");
				} else {
					puts("Thrown");
				}
			} else {
				if(p == n){
					printf("%d\n", m - (n - 1) / 2);
				} else {
					if(!((p & 1) ^ (n & 1))){
						puts("1");
					} else {
						puts("0");
					}
				}
			}
		}
	}
	return 0;
} 

1025:Switch lights(nim积)

题意:有 n n 个灯亮着,每个灯的位置为 ( x i , y i ) (x_i, y_i) ,每次可以选择 4 4 个角(可以是一个灯)把灯的状态翻转,而且右下角的灯原先必须亮着。当无法操作时,游戏结束。问先手能否必胜。

范围: n 1000   ,   1 x i , y i 1 e 4 n \le 1000~,~1 \le x_i,y_i \le 1e4

分析:nim积模板题,具体的证明可以查看这篇论文的4.4.4章节。

Code

#include<cstdio>
int m[2][2] = {0, 0, 0, 1};
int Nim_Mult_Power(int x,int y)
{
	if(x<2)
		return m[x][y];
	int a=0;
	for(;;a++)
		if(x>=(1<<(1<<a))&&x<(1<<(1<<(a+1))))
			break;	
	int m=1<<(1<<a);	
	int p=x/m,s=y/m,t=y%m;	
	int d1=Nim_Mult_Power(p,s);	
	int d2=Nim_Mult_Power(p,t);	
	return (m*(d1^d2))^Nim_Mult_Power(m/2,d1);
}
int Nim_Mult(int x,int y)
{
	if(x<y)
		return Nim_Mult(y,x);
	if(x<2)
		return m[x][y];	
	int a=0;
	for(;;a++)
		if(x>=(1<<(1<<a))&&x<(1<<(1<<(a+1))))
			break;	
	int m=1<<(1<<a);
	int p=x/m,q=x%m,s=y/m,t=y%m;
	int c1=Nim_Mult(p,s),c2=Nim_Mult(p,t)^Nim_Mult(q,s),c3=Nim_Mult(q,t);	
	return (m*(c1^c2))^c3^Nim_Mult_Power(m/2,c1);
}
int main()
{
	int t, n, x, y;
	while(~scanf("%d", &t)){
		while(t--){
			scanf("%d", &n);
			int ans = 0;
			for(int i = 0; i < n; i++){
				scanf("%d %d", &x, &y);
				ans ^= Nim_Mult(x, y);
			}
			if(ans){
				puts("Have a try, lxhgww.");
			} else {
				puts("Don't waste your time.");
			}
		}
	}
	return 0;
}

【END】感谢观看!

发布了44 篇原创文章 · 获赞 17 · 访问量 9002

猜你喜欢

转载自blog.csdn.net/qq_41765114/article/details/104367173