【组合数】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 ai. 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(1in) such that the indices of items taken away in the i-th move are different.

Input

The first line contains an integer n(1n106).

The second line contains n integers a1,a2,,an(1ain).

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想是写出线性,其实不是线性,很痛苦,思路特别杂乱,本次记录思考过程也是反思)

首先知道a1an的数值,就相当于已经知道了结果,并且结果一定是先手赢或者是平局(如果后手有赢的策略,先手一定会先拿),知道这个并没有什么用处。

由于下标不同,数值相同的数取来也能算一种不同方案,这里可以直接按照同数值的个数计数,用cnt[i]表示数值i出现的次数。设maxval为当前所剩数中的最大值,minval为当前所剩数中的最小值。

怎么想到和奇偶性质有关呢?

先不管样例1里面先手知道自己必胜所以先取相对小的数也不影响结果的操作,就用贪心的思路想一想。对过程分析一步步分析,如果maxval是奇数个,若要符合最优策略,先手必定拿走第一个和剩下的一半,若是偶数个,两人肯定对半分,这对所有的剩下的数都适用,所以奇数情况的下一步就是偶数情况,这里可以想到和数值个数有关系了,所以最优取法的过程中,每个数值一定是两个两个配对着取。

对取法过程进行分析,先不贪心,因为从大到小考虑很难分析单个过程(在这里被卡死了),不如从小的值到大的值考虑。

先手取minval,后手取剩下的数中的minval的话。

如果上一步对家没有拿当前的minval,说明当前的minval在自己这里所有的数中一定是最大,或者是和最大值相等的,所以minval可以放在当前的所取的数的策略,前面的任意一步取得。并且,由于从小到大遍历,现在自己取了minval,前面的数是都不符合贪心策略的,因为它们都比minval小,只有后面的数要符合贪心,所以前面的数在前面的过程中可以按照任意顺序取。

取法过程分析完了,求一个公式。

在当前最大值是maxval时,取maxval的过程会有cnt[maxval]!种,因为下标不同取来也算一种不同的取法

由于要符合最优策略,当前的maxval一定是贪来保证个数到 cnt[maxval]/2

但是自己取的数不一定只有maxval一种,并且不用保证最先取到maxval,只要保证maxval的个数,前面小于maxval的值可以随便拿

所以可以得知,数值由小到大遍历的过程中自身的取法个数:

i为当前maxval的值,前面小于i1的数都取到了,保证i的个数是取到了cnt[i]/2个,过程随便拿,所以这些策略取法的总和是Cj=1i1cj+ci/2ci/2

所以最后答案是:i=1ncnti!Cj=1i1cntj+cnti/2cnti/2

优化:组合数,组合数的逆元需要预处理,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;
}
posted @   TomiokapEace  阅读(357)  评论(4编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示