Educational Codeforces Round 170 (Rated for Div. 2) EFG题解

E. Card Game

题目大意

有两个人正在玩一个卡牌游戏,该游戏使用的卡牌组有 $n \times m$ 张卡牌,每张卡牌都有两个参数:花色和等级。花色编号从 $1 \sim n$,等级编号从 $1 \sim m$。每种花色和等级的组合恰有一张牌。

一张花色为 $a$,等级为 $b$ 的牌可以击败一张花色为 $c$,等级为 $d$ 的牌的条件有:

  • $a = 1, \; c \not = 1$ (花色为 $1$ 的卡牌可以打败任何其他花色的卡牌);
  • $a = c, \; b > d$ (同一花色的卡牌可以打败等级较低的卡牌)。

两名玩家进行游戏。在游戏开始之前,他们各自获得正好一半的牌组。第一名玩家获胜的条件是,对于第二名玩家的每一张卡牌,他都能选择一张可以打败它的卡牌,并且没有卡牌被重复选择。否则第二名玩家获胜。

你的任务是计算使第一名玩家获胜的卡牌分配方式数量。两种方式被认为是不同的,如果存在一张卡牌在一种方式中属于第一名玩家,而在另一种方式中属于第二名玩家。结果对 $998244353$ 取模。

$1 \leq n, m \leq 500$

题目分析

首先考虑 $n=1$ 时要如何解决这个问题。容易发现,将牌按等级从大到小排序后,此时任意合法的拿牌序列都形如一个合法的括号匹配。

然而对于 $n>1$ 时,由于除花色 $1$ 之外的花色之间无法相互抵消,从而第一个人得到的一定不多于第二个人得到的。因为一旦比第二个人多,那么他一定会剩下一些这种花色的牌无法消耗。那么对于第二个人比第一个人得到的该花色的牌多的情况,此时只能由第一个人得到的多的花色为 $1$ 的牌抵消掉。

于是考虑设计状态 $f_{i, j}$ 表示分配前 $i$ 副花色的卡牌使得第一个人手里还剩下 $j$ 张可用的 $1$ 花色卡牌的方案数。通过枚举在 $(i + 1)$ 花色第一个人需要使用的 $1$ 花色牌数 $k$,可以进行转移 $f_{i, j} * g_k \rightarrow f_{i + 1, j - k}$,其中 $g_k$ 是长为 $m$ 的括号序列多出 $k$ 个左括号(或右括号)的方案数。

时间复杂度 $O\left(nm^2\right)$

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <bits/stdc++.h>

#define R register
#define ll long long
#define sum(a, b, mod) (((a) + (b)) % mod)
#define mul(a, b, mod) (((a) * 1ll * (b)) % mod)
#define meow(cat...) fprintf(stderr, cat)

const int mod = 998244353;
const int MaxN = 5e2 + 10;

int n, m, g[MaxN][MaxN], f[MaxN][MaxN];

signed main()
{
scanf("%d%d", &n, &m), g[0][0] = 1;
for (int i = 0; i < m; i++)
for (int j = 0; j <= i; j++)
{
g[i + 1][j + 1] = sum(g[i + 1][j + 1], g[i][j], mod);
if (j) g[i + 1][j - 1] = sum(g[i + 1][j - 1], g[i][j], mod);
}
f[0][0] = 1;
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= m; j++)
{
for (int k = 0; k <= m; k++)
{
int t = i ? j - k : j + k;
if (t < 0 || t > m) continue;
f[i + 1][t] = sum(f[i + 1][t], mul(f[i][j], g[m][k], mod), mod);
}
}
}
printf("%d\n", f[n][0]);
return 0;
}

F. Choose Your Queries

题目大意

有 $1$ 个长度为 $n$,初始全部为 $0$ 的数组。有 $q$ 组操作,每次操作有两个参数 $x, y$。在每次操作中,你需要选择 $\{x, y\}$ 中的一个,并加上 $1$ 或 $-1$。需要保证操作过程中每个数都保持非负。给出一种使得最后所有数的和最小的操作方案。

$1 \leq n, q \leq 3 \times 10 ^ 5$

题目分析

注意到题目所给的结构很像一个无向图,于是考虑在无向图上处理。容易发现对于一个点 $u$,最优的决策一定是一加一减,从而只和选择 $u$ 的次数的奇偶性有关,即我们要让尽量少的点具有奇数入度。

考虑答案的下界,容易发现对于某一个联通块,答案的下界是这个联通块中边的条数模 $2$ 的余数。可以发现一定存在一种构造达到这个下界:对于每一个联通块,从某个点出发构造一棵 $\text{DFS}$ 树,然后自底向上分配边。具体过程是:对于一个节点 $v$,首先遍历它的所有子节点;然后对于 $v$ 连接的边,可以分为 $3$ 类:连接 $v$ 儿子的树边,返祖边,横叉边。我们用树边和横叉边构造边对,并把它们移除。如果最后剩下的边的数量为奇数,那么把 $v$ 和它父亲之间的边(如果还没被使用)进行配对,否则留下一条边留待 $v$ 的祖先处理。最后如果联通块的边为奇数,那么会在 $\text{DFS}$ 树的根节点留下一条未被配对的边。

有一个细节需要注意:这题需要保证任意时刻序列中所有数的值非负。所以应该把所有定向到 $u$ 的边排序后,按照题目给定的顺序轮流加一减一,才能符合题意。

时间复杂度 $O(n+q)$

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <bits/stdc++.h>

#define R register
#define ll long long
#define sum(a, b, mod) (((a) + (b)) % mod)
#define meow(cat...) fprintf(stderr, cat)

const int MaxN = 3e5 + 10;
const std::string choice = "xy";
const std::string sign = "+-";

std::string ans[MaxN];
int n, q, color[MaxN], edge[MaxN][2];
std::vector<int> g[MaxN];

void pair(int q1, int q2)
{
if (q1 > q2) std::swap(q1, q2);
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
if (edge[q1][i] == edge[q2][j])
{
ans[q1] = {choice[i], sign[0]};
ans[q2] = {choice[j], sign[1]};
return;
}
}

int dfs(int u, int pe = -1)
{
color[u] = 1;
std::vector<int> ed;
for(auto e : g[u])
{
int v = edge[e][0] ^ edge[e][1] ^ u;
if(color[v] == 1) continue;
if(color[v] == 0)
{
if(dfs(v, e))
ed.push_back(e);
}
else ed.push_back(e);
}
int res = true;
if(ed.size() % 2 != 0)
{
if(pe != -1) ed.push_back(pe);
else ed.pop_back();
res = false;
}
for(int i = 0; i < ed.size(); i += 2)
pair(ed[i], ed[i + 1]);
color[u] = 2;
return res; // return true if parent edge still exists
}

signed main()
{
scanf("%d%d", &n, &q);
for (int i = 1; i <= q; i++)
{
scanf("%d%d", &edge[i][0], &edge[i][1]);
g[edge[i][0]].push_back(i);
g[edge[i][1]].push_back(i);
ans[i] = "x+";
}
for(int i = 1; i <= n; i++)
if(!color[i]) dfs(i);
for(int i = 1; i <= q; i++)
printf("%s\n", ans[i].c_str());
return 0;
}

G. Variable Damage

题目翻译

Monocarp 正在组建一支军队,在一款电子游戏中与龙作战。

军队由两个部分组成:英雄和防御神器。每个英雄都有一个参数——他的生命值。每个防御神器也有一个参数——它的耐久度。

在战斗开始前,Monocarp 将神器分配给英雄,使得每个英雄最多持有一个神器。

战斗由若干回合组成,每个回合的流程如下:

  • 首先,龙对每个英雄造成伤害,伤害值等于 $\frac{1}{a+b}$(为不进行取整的实数),其中 $a$ 是存活英雄的数量,$b$ 是活跃神器的数量;
  • 之后,所有生命值小于或等于 0 的英雄死亡;
  • 最后,一些神器会被取消激活。当满足以下任意一个条件时,耐久度为 $x$ 的神器被取消激活:持有该神器的英雄死亡,或者该英雄累计承受的伤害达到 $x$。如果神器未被任何英雄持有,则在战斗开始时即为非活跃状态。

当没有英雄存活时,战斗结束。

最初,军队为空。共有 $q$ 个查询:添加一个生命值为 $x$ 的英雄或耐久度为 $y$ 的神器到军队。对于每个查询,确定如果 Monocarp 最优地分配神器,他能存活的最大回合数。

$1 \leq q \leq 3 \times 10 ^ 5$

题目解析

首先考虑给定 $a, b$ 数组时如何解决该问题。我们先把神器分配给英雄,组成对 $a_i, b_i$。如果 $m > n$,那么丢弃 $b$ 最小的一些神器。否则如果 $m < n$,用 $b_i = 0$ 补充缺少的神器。

注意到一个有着神器 $b_i$ 的英雄 $a_i$ 可以被替换为一个有着 $a_i$ 点血量的英雄和一个有着 $\min \left(a_i, b_i\right)$ 点血量的英雄,而不会改变答案。

于是我们可以考虑把问题转化为神器不存在,只有 $2n$ 个血量分别为 $a_1, \min (a_1, b_1), \dots, a_n, \min (a_n, b_n)$ 的英雄,每回合龙可以对英雄造成总计一点的伤害,因此最终答案就是:

第一个和容易维护,我们考虑如何维护第二个和的最小值。容易发现,将神器和英雄均降序排序后两两配对是最优的。现在考虑动态插入时要怎么维护。

考虑类似扫描线的思路。我们将英雄和神器组合成一个数组并按降序排列。为了简化起见,假设所有耐久度和生命值都是不同的整数,这可以通过离散化实现。把英雄当成 1,让 $s_{a_i}$ 加上 1,武器当成 $-1$,让 $s_{b_i}$ 减去 1,那么一个血量为 $a_i$ 的英雄会造成贡献(即比与其匹配的武器的 $b$ 小)当且仅当 $\sum_{k=a_i}^{\infty} s_k \leq 0$,同样的,一个强度为 $b_i$ 的装备会造成贡献当且仅当 $\sum_{k=b_i}^{\infty} s_k \geq 0$。

现在考虑维护 $s$ 的后缀和数组 $f$,并查询 $f$ 上所有 $\leq/ \ge 0$ 的位置对应的权值,修改则是区间 $+1/-1$。考虑通过分块维护,显然查询的时间复杂度为 $O(\dfrac{q}{B})$。对于修改,我们考虑块内维护差分数组,由于块内任意两个 $f$ 值的差小于块长 $B$,于是空间复杂度是线性的。在块内修改的时间复杂度是 $O(B)$,重新计算答案的时间复杂度为 $O(\dfrac{q}{B})$。于是取 $B = \sqrt q$,那么总时间复杂度为 $O(q \sqrt q)$

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <bits/stdc++.h>

#define forn(i, n) for (int i = 0; i < int(n); i++)

using namespace std;

struct query
{
int t, v;
};

int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
int m;
cin >> m;
vector<query> q(m);
forn(i, m) cin >> q[i].t >> q[i].v;

vector<pair<int, int>> xs;
forn(i, m) xs.push_back({q[i].v, i});
sort(xs.rbegin(), xs.rend());
forn(i, m) q[i].v = xs.rend() - lower_bound(xs.rbegin(), xs.rend(), make_pair(q[i].v, i)) - 1;

const int p = sqrt(m + 10);
const int siz = (m + p - 1) / p;

vector<int> tp(m);
vector<int> val(m);
vector<vector<long long>> dp(p, vector<long long>(2 * siz + 1));
vector<int> blbal(p);

auto upd = [&](const query &q)
{
tp[q.v] = q.t;
val[q.v] = xs[q.v].first;
blbal[q.v / siz] += q.t == 1 ? 1 : -1;
};

auto recalc = [&](int b)
{
dp[b].assign(2 * siz + 1, 0);
int bal = 0;
for (int i = b * siz; i < m && i < (b + 1) * siz; ++i)
{
if (tp[i] == 1)
{
dp[b][0] += val[i];
dp[b][0] += val[i];
dp[b][-bal + siz] -= val[i];
++bal;
}
else if (tp[i] == 2)
{
dp[b][-bal + 1 + siz] += val[i];
--bal;
}
}
forn(i, 2 * siz)
{
dp[b][i + 1] += dp[b][i];
}
};

auto get = [&](int b, int bal)
{
bal += siz;
if (bal < 0) return dp[b][0];
if (bal >= 2 * siz + 1) return dp[b].back();
return dp[b][bal];
};

for (auto it : q)
{
upd(it);
recalc(it.v / siz);
int bal = 0;
long long ans = 0;
for (int i = 0; i * siz < m; ++i)
{
ans += get(i, bal);
bal += blbal[i];
}
cout << ans << '\n';
}

return 0;
}
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×