【組合數】2021ICPC濟南補題-C Optimal Strategy


題目鏈接

Description

Ena and Mizuki are playing a game.

There are n items in front of them, numbered from \(1\) to \(n\). The value of the \(i\)-th item is \(a_i\). Ena and Mizuki take turns to move, while Ena moves first. In a move, the player chooses an item that has not been taken and takes it away. The game ends when all items are taken away. The goal of either player is to maximize the sum of values of items they have taken away.

Given that both players move optimally, how many possible game processes are there? Since the number may be too large, you should output it modulo \(998244353\).

Two processes are considered different if there exists some integer \(i(1≤i≤n)\) such that the indices of items taken away in the \(i\)-th move are different.

Input

The first line contains an integer \(n (1≤n≤10^6)\).

The second line contains n integers \(a_1,a_2,…,a_n (1≤a_i≤n)\).

Output

Output the answer.

Sample Input 1

3
1 2 2

Sample Output 1

4

Sample Input 2

6
1 3 2 2 3 1

Sample Output 2

120

Sample Input 3

12
1 1 4 5 1 4 1 9 1 9 8 10

Sample Output 3

28800

Explanations

In the first example, there are four possible processes:

  • \([1,2,3]\).
  • \([1,3,2]\).
  • \([2,3,1]\).
  • \([3,2,1]\).

Here \([a,b,c]\) means that in the first move Ena takes away the \(a\)-th item, in the second move Mizuki takes away the \(b\)-th item, and in the final move Ena takes away the \(c\)-th item.

Note that \([2,1,3]\) is not a possible process, since the second move is not optimal for Mizuki.

題意

給定n個物品和其價值(數值可以重復),Ena和Mizuki輪流取物品,每個人的得分是其所取物品的價值總和,Ena先手,每個人取的都是當前的最優,問到過程結束(結果不變)中間有多少種取法的過程。

思路

(直通車:官方的一句話題解,考慮最大值如果是偶數個,那么會每次被兩個兩個的取;如果是奇數個,那么會被先手立刻取走一個,變成偶數的情況。)

(賽中看到1e6想是寫出線性,其實不是線性,很痛苦,思路特別雜亂,本次記錄思考過程也是反思)

首先知道\(a_1\)\(a_n\)的數值,就相當於已經知道了結果,並且結果一定是先手贏或者是平局(如果后手有贏的策略,先手一定會先拿),知道這個並沒有什么用處。

由於下標不同,數值相同的數取來也能算一種不同方案,這里可以直接按照同數值的個數計數,用\(cnt[i]\)表示數值\(i\)出現的次數。設\(maxval\)為當前所剩數中的最大值,\(minval\)為當前所剩數中的最小值。

怎么想到和奇偶性質有關呢?

先不管樣例1里面先手知道自己必勝所以先取相對小的數也不影響結果的操作,就用貪心的思路想一想。對過程分析一步步分析,如果\(maxval\)是奇數個,若要符合最優策略,先手必定拿走第一個和剩下的一半,若是偶數個,兩人肯定對半分,這對所有的剩下的數都適用,所以奇數情況的下一步就是偶數情況,這里可以想到和數值個數有關系了,所以最優取法的過程中,每個數值一定是兩個兩個配對着取。

對取法過程進行分析,先不貪心,因為從大到小考慮很難分析單個過程(在這里被卡死了),不如從小的值到大的值考慮。

先手取\(minval\),后手取剩下的數中的\(minval\)的話。

如果上一步對家沒有拿當前的\(minval\),說明當前的\(minval\)在自己這里所有的數中一定是最大,或者是和最大值相等的,所以\(minval\)可以放在當前的所取的數的策略,前面的任意一步取得。並且,由於從小到大遍歷,現在自己取了\(minval\),前面的數是都不符合貪心策略的,因為它們都比\(minval\)小,只有后面的數要符合貪心,所以前面的數在前面的過程中可以按照任意順序取。

取法過程分析完了,求一個公式。

在當前最大值是\(maxval\)時,取\(maxval\)的過程會有\(cnt[maxval]!\)種,因為下標不同取來也算一種不同的取法

由於要符合最優策略,當前的\(maxval\)一定是貪來保證個數到 \(\lfloor cnt[maxval]/2\rfloor\)

但是自己取的數不一定只有\(maxval\)一種,並且不用保證最先取到\(maxval\),只要保證\(maxval\)的個數,前面小於\(maxval\)的值可以隨便拿

所以可以得知,數值由小到大遍歷的過程中自身的取法個數:

\(i\)為當前\(maxval\)的值,前面小於\(i-1\)的數都取到了,保證\(i\)的個數是取到了\(\lfloor cnt[i]/2\rfloor\)個,過程隨便拿,所以這些策略取法的總和是\(C_{\sum_{j=1}^{i-1}c_j+\lfloor c_i/2\rfloor }^{\lfloor c_i/2 \rfloor}\)

所以最后答案是:\(\sum_{i=1}^n cnt_i!C_{\sum_{j=1}^{i-1}cnt_j+\lfloor cnt_i/2\rfloor }^{\lfloor cnt_i/2 \rfloor}\)

優化:組合數,組合數的逆元需要預處理,\(cnt[j]\)的求和可以用前綴和優化,細節問題就是取模之類。

復雜度\(O(n)\)

AC代碼

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define llinf 0x3f3f3f3f3f3f3f3f
#define int long long
#define ull unsigned long long
#define PII pair<int,int>
#define endl '\n'
const int N = 1e6 + 10;
const int mod = 998244353;
const double pi = acos(-1.0);
typedef long long ll;
using namespace std;
int t, n;
int fact[N];
int invfact[N];
int presum[N];
int a[N];
int cnt[N];
int quickpow(int a, int b) {//快速冪
	int res = 1;
	while (b > 0) {
		if (b & 1) res = res * a % mod;
		b >>= 1;
		a = a * a % mod;
	}
	return res;
}
int getinv(int a) { return quickpow(a, mod - 2); }//費馬小定理求逆元
void init() {//預處理階乘和階乘的逆元
	fact[0] = 1;
	for (int i = 1; i < N; i++) {
		fact[i] = fact[i - 1] * i % mod;
	}
	invfact[0] = 1;
	invfact[N - 1] = quickpow(fact[N - 1], mod - 2);
	for (int i = N - 2; i > 0; --i) {
		invfact[i] = invfact[i + 1] * (i + 1) % mod;
	}
	return;
}
int C(int n, int m) {//組合數
	return fact[n] * invfact[m] % mod * invfact[n - m] % mod; 
}
void solve() {
	for (int i = 0; i <= n; i++) {//初始化
		a[i] = 0;
		cnt[i] = 0;
	}
	for (int i = 1; i <= n; i++) {//輸入
		cin >> a[i];
		cnt[a[i]] ++;
	}
	presum[1] = cnt[1];
	for (int i = 2; i <= n; i++) {//cnt[i]的前綴和
		presum[i] = presum[i - 1] + cnt[i];
	}
	int sum = 1;
	for (int i = 1; i <= n; i++) {//按公式求和
		sum = sum * fact[cnt[i]] % mod * C(presum[i - 1] - presum[0] + cnt[i] / 2, cnt[i] / 2) % mod;
	}
	cout << (sum) % mod << endl;//輸出
	return;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	init();
	while (cin >> n) {
		solve();
	}
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM