蓋世計劃-0724-B班模擬 C. 遊戲 (game)

Fire_Raku發表於2024-07-25

首先,Alice 先去 \(n\) 個商店中購買物品。其中第 \(i\) 個商店售賣編號為 \(i\) 的物品,且每個物品的售價為 \(a_i\)。Alice 的總花費不能超過 \(k\)

接著,Bob 再去另外 \(m\) 個商店中購買物品。其中第 \(i\) 個商店售賣編號為 \(n+i\) 的物品,且每個物品的售價為 \(1\)。Bob 的總花費不能超過 \(m\)

然後,Alice 和 Bob 將他們買到的物品放在一起。然後從 Alice 開始,他們輪流進行如下操作:

  • 從剩餘的物品中取走若干個物品(至少一個),取走的物品必須編號相同。

最後無法操作的人輸。問 Alice 最初有多少種購買方式,使得 Alice 在後續的遊戲中有必勝策略。Alice 的兩種購買方式不同當且僅當兩種方案中 Alice 在某一個商店中購買的物品的數量不同。答案對 \(10^9+7\) 取模。

\(1\le n,a_i\le 100\)\(0\le k\le m\le 10^{18}\)

一看,這遊戲不就是 nim 遊戲嘛,有結論:

設有 \(n\) 堆石子,第 \(i\) 堆石子數量為 \(a_i\),當且僅當 \(\bigoplus\limits_{i=1}a_i\) 不為 \(0\) 時先手必勝。

分析題目的限制,第一個限制可以寫成 \(\sum a_ic_i\le k\),第二個限制是啥。

我們假如 Alice 取出的石子為 \(c_1...c_L\),記 \(s=\bigoplus\limits_{i=1}c_i\),那麼當 \(s\le m\) 的時候,顯然 Bob 可以取遍 \([1,m]\) 中的任何數,於是就輸了。

所以分析完,限制就是 \(s\ge m\)。我們要求的方案滿足:

  1. \(s_1=\sum\limits_{i=1}^L a_ic_i\le k\)
  2. \(s_2=\bigoplus\limits_{i=1}^Lc_i>m\)

一看新題,又不會了。\(k\)\(m\) 都特別大。我們應該想到數位 dp,它也許可以計算出方案數。考慮按位考慮 \(c_i\) 二進位制上的每一位,像列豎式計算一樣,這樣一列上每一位都是為 \(0\)\(1\) 的值。 按位列舉需要新考慮的問題,一般列入狀態中轉移:

  1. 進位
  2. 與上界的大小關係情況

第一個限制就變成 01 揹包問題了,我們對這一位求揹包就可以在 \(O(n\sum a_i)\) 的時間內計算出進位的問題。

第二個限制關心的實際上就是揹包取數的數量奇偶性吧,所以揹包的狀態加一位表示目前取物數量的奇偶性即可。

\(f_{i,j,0/1,0/1}\) 表示考慮了低 \(i\) 位,最終向 \(i+1\) 位的進位為 \(j\)\(s_1\)\(i\) 位與 \(k\)\(i\) 位的大小關係為 \(0/1\)\(s_2\)\(i\) 位與 \(k\)\(i\) 位的大小關係為 \(0/1\) 的方案數。顯然 \(i\) 一維可以滾動。

這裡的列舉順序為從低位到高位,與數位 dp 不同,有時這樣的狀態更加方便。

時間複雜度為 \(O(n\log V\sum a_i)\)。難題啊,對著 std 才勉強看懂的。

#include <bits/stdc++.h>
#define pii std::pair<int, int>
#define mk std::make_pair
#define fi first
#define se second
#define pb push_back

using i64 = long long;
using ull = unsigned long long;
const i64 iinf = 0x3f3f3f3f, linf = 0x3f3f3f3f3f3f3f3f;
/*

*/
const int N = 110, mod = 1e9 + 7; 
int n;
int a[N], s;
i64 k, m;
i64 f[2][N * N][2][2], g[N * N * 2][2];
bool comp(int x, int y, int z) {
	if(x > y) return 1;
	if(x == y && z) return 1;
	return 0; //比較陣列
}
int main() {
	freopen("game.in", "r", stdin);
	freopen("game.out", "w", stdout);

    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
	std::cin >> n >> k >> m;

	for(int i = 1; i <= n; i++) {
		std::cin >> a[i];
		s += a[i];
	}

	int cur = 1, lst = 0; //滾動陣列
	f[cur][0][0][0] = 1; //初始化
	for(int b = 0; b < 60; b++) { //按位 dp
		std::swap(cur, lst); 
		for(int kl = 0; kl < 2; kl++) { //列舉先前與 k 的大小關係
			for(int ml = 0; ml < 2; ml++) { //列舉先前與 m 的大小關係
				for(int i = 0; i <= s; i++) g[i][0] = f[lst][i][kl][ml], f[lst][i][kl][ml] = 0; //揹包初始化,繼承先前的進位貢獻
 				int nws = s; //先前的進位上界
				for(int i = 1; i <= n; i++) {
					nws += a[i]; 
					for(int j = nws; j >= 0; j--) {
						for(int k = 0; k < 2; k++) { //列舉取的數量的奇偶性 
							if(g[j - a[i]][k ^ 1] && j - a[i] >= 0) g[j][k] = (g[j][k] + g[j - a[i]][k ^ 1]) % mod; //揹包
						}
					}
				}

				for(int i = 0; i <= (s << 1); i++) { //列舉進位,注意是兩倍,形如 1+1/2+1/4+1/8...<=2
					for(int j = 0; j < 2; j++) { //列舉異或後這一位是 0/1
						int x = comp(i & 1, ((k >> b) & 1), kl), y = comp(j, ((m >> b) & 1), ml); //判斷大小
						f[cur][i >> 1][x][y] = (f[cur][i >> 1][x][y] + g[i][j]) % mod; //轉移,到下一位時進位/2
						g[i][j] = 0; //清空
					}
				}
			}
		}
	}

	std::cout << f[cur][0][0][1] << "\n"; //答案,表示進位為 0,sum(ac)<=k且xorsum(c)>m的方案數

	return 0;
}

相關文章