BZOJ 1413 [ZJOI2009] [洛谷2599] 取石子游戏 (博弈论 dp )

这个题数据有点水,有个奇葩程序可以过。。。(也不知道为什么。。。)

/*************************************************************************
    > Author: wzw-cnyali
    > Created Time: 2017/3/8 21:23:17
 ************************************************************************/

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<algorithm>

using namespace std;

typedef long long LL;

#define REP(i, a, b) for(register int i = (a), i##_end_ = (b); i <= i##_end_; ++ i)
#define DREP(i, a, b) for(register int i = (a), i##_end_ = (b); i >= i##_end_; -- i)
#define debug(...) fprintf(stderr, __VA_ARGS__)
#define mem(a, b) memset((a), b, sizeof(a))

template<typename T> inline bool chkmin(T &a, const T &b) { return a > b ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, const T &b) { return a < b ? a = b, 1 : 0; }

int read()
{
	int sum = 0, fg = 1; char c = getchar();
	while(c < '0' || c > '9') { if (c == '-') fg = -1; c = getchar(); }
	while(c >= '0' && c <= '9') { sum = sum * 10 + c - '0'; c = getchar(); }
	return sum * fg;
}

const int Size = 100000;
const int inf = 0x3f3f3f3f;

int a[Size];

int main()
{
	int Case = read();
	while(Case--)
	{
		int n = read();
		REP(i, 1, n)
		{
			a[i] = read();
		}
		if(abs(a[1] - a[n]) <= 1) printf("0\n");
		else printf("1\n");
	}
    return 0;
}

可以无视这种AC代码。
接下来来正经的谈正解:
首先,讲个性质,取石子时,当先手使得左右两边的石子相等时,可以保证先手必胜,因为当先手取到左右石子相等的局面时,则后手无论在哪一堆取石子,接下来先手都可以有相同的方法取另一堆石子,这样又回到了原来的状态(左右石子相等),则无论如何先手都有办法应对后手的取法。

设在区间[i, j]左边放left[i][j](可以为0)个石子后(区间内石子保持原状),产生的局面为先手必败局面(这是重点),right[i][j]就是在右边放(定义类似);设a[i]为第i堆石子个数。
最后我们只用看  left[2][n]  是否等于  a[1]  就可以了。(如果等于就说明这个情况先手必败,否则先手必胜)

这一题可以注意到对于一段区间[L, R], 若L+1到R的石子数固定, 那么使得在这段区间上先手必败的a[L]有且仅有一个。
接下来证明left[i][j]与(right[i][j]同理)是唯一的:
1)若存在两或以上个left[i][j],则从某一个必定可一步转移到另一个,矛盾,故最多只有一个;
2)若没有left[i][j],那么必胜态只可能通过拿右边的石子转移到某个必败态,由于此时左边石子可以是任意个,右边石子数固定,于是必有右边的某一个石子数对应了多种必败态,这些必败态只有左边的石子数不同,与1矛盾。

最后我们考虑left[i][j](同right[i][j])的求法,大力分类讨论:
设  L = left[i][j - 1]  ,   R = right[i][j - 1]  ,   X = a[j]  . 通过下面的分析我们可以发现left[i][j]只和L, R, X三个数有关.

1)边界条件:left[i][i] = right[i][i] = a[i],两堆一样的,后手照着先手拿的数量在另一堆拿即可。
2)首先, 最容易想到的是, R = X的情况, 这时[i, j]这段区间已经先手必败, 那么left[i][j] = 0。
3)若 x  < L && x < R, left[i][j] = x, 因为此时当两堆石子相同时,后手照着先手取,这样先手一定会先拿完某一堆,后手占据主动,这之后又会出现某一种我们分类讨论中的情况,相当于一种递归,直到后手获胜。
4)若 x < L && x > R, left[i][j] = x - 1。此时如果先手在左边拿使其个数变为y,若y < R,则后手在右边也拿到y即可变为情况3;若y >= R,则后手在右边拿到y + 1, 于是回到了相同的情况。如果先手在右边拿到y,若y < R,同样可到情况3;若y = R, 直接把左边的堆拿完,先手便面临败局;若y > R,则在左边拿到y - 1,回到相同情况。
5)若 x < R && x >= L, left[i][j] = x + 1, 与情况4恰好相反。
6)若 x > L && x > R,left[i][j] = x。若先手将某一堆拿到了y,且y > R, 跟着先手拿即可回到相同情况;若y < L,回到情况3;若在在左边拿到L或在右边拿到R,后手拿掉另一堆即可;否则根据情况在另一堆拿到y - 1或是y + 1,分别对应情况4和5。

right[i][j]的求法与left[i][j]完全对称,最后只需判断 a[1] == left[2][n]。

代码如下C++:

/*************************************************************************
    > Author: wzw-cnyali
    > Created Time: 2017/3/9 20:37:10
 ************************************************************************/

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<algorithm>

using namespace std;

typedef long long LL;

#define REP(i, a, b) for(register int i = (a), i##_end_ = (b); i <= i##_end_; ++ i)
#define DREP(i, a, b) for(register int i = (a), i##_end_ = (b); i >= i##_end_; -- i)
#define debug(...) fprintf(stderr, __VA_ARGS__)
#define mem(a, b) memset((a), b, sizeof(a))

template<typename T> inline bool chkmin(T &a, const T &b) { return a > b ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, const T &b) { return a < b ? a = b, 1 : 0; }

int read()
{
	int sum = 0, fg = 1; char c = getchar();
	while(c < '0' || c > '9') { if (c == '-') fg = -1; c = getchar(); }
	while(c >= '0' && c <= '9') { sum = sum * 10 + c - '0'; c = getchar(); }
	return sum * fg;
}

const int Size = 1010;
const int inf = 0x3f3f3f3f;

int a[Size];
int Left[Size][Size], Right[Size][Size];

int main()
{
#ifndef ONLINE_JUDGE
	freopen("input.in", "r", stdin);
	freopen("output.out", "w", stdout);
#endif
	int Case = read();
	while(Case--)
	{
		int n = read();
		REP(i, 1, n)
		{
			Left[i][i] = Right[i][i] = a[i] = read();
		}
		REP(k, 1, n - 1)
		{
			REP(i, 1, n - k)
			{
				int j = i + k, L, R, z;

				L = Left[i][j - 1], R = Right[i][j - 1], z = a[j];
				if(z == R) Left[i][j] = 0;
				else if((z < L && z < R) || (z > L && z > R)) Left[i][j] = z;
				else if(z >= L && z < R) Left[i][j] = z + 1;
				else Left[i][j] = z - 1;

				L = Left[i + 1][j], R = Right[i + 1][j], z = a[i];
				if(z == L) Right[i][j] = 0;
				else if((z < L && z < R) || (z > L && z > R)) Right[i][j] = z;
				else if(z >= R && z < L) Right[i][j] = z + 1;
				else Right[i][j] = z - 1;
			}
		}
		printf("%d\n", a[1] == Left[2][n] ? 0 : 1);
	}
    return 0;
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章