包含本场比赛的 $\text{E}, \text{F}, \text{G}$ 三道题。
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;  }  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 ; }