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;
}



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