1. 程式人生 > >Light OJ-1344 Aladdin and the Game of Bracelets DP(記憶化搜尋) + SG函式 博弈

Light OJ-1344 Aladdin and the Game of Bracelets DP(記憶化搜尋) + SG函式 博弈

題目描述

It’s said that Aladdin had to solve seven mysteries before getting the Magical Lamp which summons a powerful Genie. Here we are concerned about the fourth mystery.


In the cave, Aladdin was moving forward after passing the pathway of magical stones. He found a door and he was about to open the door, but suddenly a Genie appeared and he addressed himself as the guardian of the door. He challenged Aladdin to play a game and promised that he would let Aladdin go through the door, if Aladdin can defeat the Genie. Aladdin defeated the Genie and continued his journey. However, let’s concentrate on the game.


The game was called ‘Game of Bracelets’. A bracelet is a linear chain of some pearls of various weights. The rules of the game are:

1)There are n bracelets.
2)Players alternate turns.
3)In each turn, a player has to choose a pearl from any bracelet. Then all the pearls from that bracelet, that were not lighter than the pearl, will be removed. It may create some smaller bracelets or the bracelet will be removed if no pearl is left in the bracelet.
4)The player, who cannot take a pearl in his turn, loses.


For example, two bracelets are: 5-1-7-2-4-5-3 and 2-1-5-3, here the integers denote the weights of the pearls. Suppose a player has chosen the first pearl (weight 5) from the first bracelet. Then all the pearls that are not lighter than 5, will be removed (from first bracelet). So, 5-1-7-2-4-5-3, the red ones will be removed and thus from this bracelet, three new bracelets will be formed, 1, 2-4 and 3. So, in the next turn the other player will have four bracelets: 1, 2-4, 3 and 2-1-5-3. Now if a player chooses the only pearl (weight 3) from the third bracelet, then the bracelet will be removed.

Now you are given the information of the bracelets. Assume that Aladdin plays first, and Aladdin and the Genie both play optimally. Your task is to find the winner.

樣例輸入

4
2
7 5 1 7 2 4 5 3
4 2 1 5 4
2
2 5 2
2 5 2
1
5 5 2 5 2 5
3
5 5 2 5 2 5
5 7 2 7 3 2
4 5 1 5 4

樣例輸出

Case 1: Aladdin
(2 5)
Case 2: Genie
Case 3: Aladdin
(1 2)(1 5)
Case 4: Aladdin
(2 7)(3 1)(3 5)

題意

有 n 個手鐲,每個手鐲上都有很多帶有權值的珍珠。Aladdin 和 Genie 輪流操作,在其中一個手鐲上選擇一顆珍珠,然後被選擇的這顆珍珠和這個手鐲上權值大於或等於他的珍珠都會被刪除。這樣子一次操作後,這個手鐲就會變成很多小手鐲,或者如果手鐲上沒有珍珠了,手鐲就會被刪除。如果最後誰不能操作,誰就輸了。最後輸出第一步可以必勝的操作 “(選擇的手鐲,選擇的權值)”。

題解

解題主要分為兩部分,一是解決輸贏,二是解決必勝的取珍珠問題。

第一部分

對於第一部分,由於每個手鐲都是獨立互不干擾的,所以我們可以考慮求出每個手鐲的 SG 值,然後異或起來,如果最後異或的結果不為 0,則先手必勝,否則後手勝。
如此一來,難點變成了求 SG 值。由於每次輸入的權值不同,會導致分割節點的不同,所以我們如果在輸入權值之前就列舉所有分割情況,複雜度太高。所以我們選擇在輸入權值後,列舉手鐲上的權值,進行分割,然後記憶化搜尋。最後如果手鐲上的珍珠有 0 個,必輸,此時 SG = 0。如果有一個,必勝 SG = 1;
我們考慮用 SG[i][j][k]SG[i][j][k] 來儲存第 ii 個手鐲,區間為 [j,k][j, k] 的 SG 值進行記憶化搜尋,也就是DP,如果不瞭解記憶化搜尋,可以去學習一下。
記憶化搜尋 SG 值框架:

memset(SG, -1, sizeof SG);
int find(int num, int l, int r) {//對第num條項鍊,區間為 [l,r] 進行搜尋
	if (SG[num][l][r] != -1) return SG[num][l][r]; //如果已經被搜尋過,直接返回
	if (l == r) return SG[num][l][r] = 1; //如果手鐲只有一個珍珠,SG賦值為1,並返回
	if (r < l) return SG[num][l][r] = 0; //如果手鐲沒有珍珠,必輸,返回0
	int now_SG = 0;
	//如果不滿足以上條件,則尋找這種情況的SG值。
	return SG[num][l][r] = now_SG;
}

現在的問題只剩下,如果不符合以上情況,應該怎麼去找 SG 值,我們可以去列舉這個手鐲上所有珍珠,假設去掉這個珍珠,他的SG值是多少。然後獲得一個mex_SG的集合,那他的SG就是不屬於這個集合的最小非負整數。
我們知道如果一個手鐲 5-1-7-2-4-5-3,如果我們選擇了 5,那就會刪去 5,7,5,然後變成了 1,2-4,3 三個手鐲,那此時去掉 5 的情況下的 SG 值就會 1,2-4,3,這三個手鐲的異或值。
記憶化搜尋SG值:

memset(SG, -1, sizeof SG);
int find(int num, int l, int r) {//對第num條項鍊,區間為 [l,r] 進行搜尋
	if (SG[num][l][r] != -1) return SG[num][l][r]; //如果已經被搜尋過,直接返回
	if (l == r) return SG[num][l][r] = 1; //如果手鐲只有一個珍珠,SG賦值為1,並返回
	if (r < l) return SG[num][l][r] = 0; //如果手鐲沒有珍珠,必輸,返回0
	int now_SG = 0;
	//定義vis記錄mex_SG集合
	bool vis[maxn]; memset(vis, false, sizeof vis);//vis只能在函式體內定義。
	//列舉每一顆珍珠
	for (int i = l; i <= r; i++) {
		int tmp = 0;// 記錄當前的情況下的SG值
		int t = 現在這個珍珠的權值;
		for (int k = 0; k < 分成的區間; k++) {
			//遍歷所有分割的區間
			tmp ^= find(num, k_l, k_r);//找第Num條手鐲,區間為[k_l, k_r]的 SG 值
		}
		vis[tmp] = true;//標記SG值
	}
	//對應mex_SG找到這個區間的SG值
	for (int i = 0; i < maxn; i++) {
		if (vis[i] == false) {
			now_SG = i; break;
		}
	}
	return SG[num][l][r] = now_SG;
}

第二部分

解決第二部分問題的前提是,知道輸贏的結果,如果是Genie贏,則跳過。Aladdin贏,則判斷。
這個判斷我們也是列舉所有的珍珠,看選擇這顆珍珠是否必勝,如果必勝,則記錄下來。

最後排序,去重,然後輸出。

那我們假設我們已經的得到了所有手鐲的異或值為 ans (ans > 0)。
當我們列舉第 num 條手鐲時,我們就先用 ans = ans ^ SG[num][1][len_num],這個結果是消除了第 num 條手鐲的影響,也就是除了第 num 條手鐲之外的其他手鐲的異或值。
然後我們得到了第 num 條手鐲選擇了某顆珍珠的 SG 值為 now_SG。
那麼我們就將後來的 ans ^ now_SG,如果結果為 0,則說明選擇這顆珍珠必勝。
總的來說就是第一部分的 ans ^ SG[num][1][len_num] ^ now_SG。
我們就可以列舉所有的珍珠,如果可以使得最後的異或結果為 0 則加入到答案中,後來排序,去重再輸出。

問題: 為什麼異或結果為 0 必勝?
因為最後計算出來的結果是選擇了這顆珍珠之後的輸贏狀態。Aladdin 選擇了這顆珍珠,則到了 Genie 先手,此時 ans = 0,先手必敗,則Genie 必敗,因此 Aladdin 必勝。

程式碼

在程式碼中,我使用了 a[i][j] 來表示第 i 條手鐲第 j 個珍珠的權值,用 a[i][0] 儲存第 i 條手鐲的長度。
用 node 結構體來儲存最後的答案,然後過載了 < 和 == ,用於答案的排序和去重。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 55;
int sg[maxn][maxn][maxn], a[maxn][maxn], n, T, ca;

struct node {
  int a, b;
  node(int c = 0, int d = 0) {a = c, b = d;}
  bool operator < (node &t) const {
    if (a == t.a) return b < t.b;
    return a < t.a;
  }
  bool operator == (node &t) const {
    return (a == t.a && b == t.b);
  }
} res[maxn * maxn];

int findSG(int num, int l, int r) {
  if (sg[num][l][r] != -1) return sg[num][l][r];
  if (r == l) {
    return sg[num][l][r] = 1;
  }
  if (r < l) {
    return sg[num][l][r] = 0;
  }

  bool vis[maxn];
  memset(vis, false, sizeof vis);
  for (int i = l; i <= r; i++) {
    int t = a[num][i], tmp[maxn], cnt = 0, ans = 0;
    //記錄比 t 大的 所有分割點
    tmp[cnt++] = l - 1;
    for (int p = l; p <= r; p++) {
      if (a[num][p] >= t) tmp[cnt++] = p;
    }
    tmp[cnt++] = r + 1;

    for (int p = 1; p < cnt; p++) {
      ans ^= findSG(num, tmp[p - 1] + 1, tmp[p] - 1);
    }
    vis[ans] = true;
  }
  for (int i = 0; i < maxn; i++) {
    if (vis[i] == false) {
      sg[num][l][r] = i; break;
    }
  }
  return sg[num][l][r];
}

int main()
{
  scanf("%d", &T);
  while (T--) {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
      scanf("%d", &a[i][0]);
      for (int j = 1; j <= a[i][0]; j++) {
        scanf("%d", &a[i][j]);
      }
    }
    memset(sg, -1, sizeof sg);
    int ans = 0;
    for (int i = 0; i < n; i++) {
      ans ^= findSG(i, 1, a[i][0]);
    }

    printf("Case %d: ", ++ca);
    if (ans) printf("Aladdin\n");
    else printf("Genie\n");

    if (ans) {
      int cnt = 0;
      for (int i = 0; i < n; i++) {
        // 列舉手鐲
        for (int j = 1; j <= a[i][0]; j++) {
          // 列舉選中的珍珠
          int ret = ans ^ sg[i][1][a[i][0]];
          int t = a[i][j], tmp[maxn], k = 0;
          //記錄比 t 大的 所有分割點
          tmp[k++] = 0;
          for (int p = 1; p <= a[i][0]; p++) {
            if (a[i][p] >= t) tmp[k++] = p;
          }
          tmp[k++] = a[i][0] + 1;
          for (int p = 1; p < k; p++) {
            ret ^= findSG(i, tmp[p - 1] + 1, tmp[p] - 1);
          }
          //與其他相連進行Nim和
          if (!ret) {
            res[cnt++] = node(i, t);
          }
        }
      }
      sort(res, res + cnt);
      cnt = unique(res, res + cnt) - res;
      for (int i = 0; i < cnt; i++) {
        printf("(%d %d)", res[i].a + 1, res[i].b);
      }
      printf("\n");
    }
  }

  return 0;
}
/*
4
2
7 5 1 7 2 4 5 3
4 2 1 5 4
2
2 5 2
2 5 2
1
5 5 2 5 2 5
3
5 5 2 5 2 5
5 7 2 7 3 2
4 5 1 5 4
*/