AtCoder Beginner Contest 242

AtCoder Beginner Contest 242

C.(动态规划,计数)1111gal -password

给定N,在可用位为N中有多少个数满足给定条件

条件1:不含0

条件2:相邻位数字之差不超过1

dp_{i, j} 为第 i 位数字中以数字j结尾的合法方案数

状态转移方程:

dp_{i, j} = dp_{i - 1, j - 1} + dp_{i - 1, j}+dp_{i - 1, j + 1}

边界: dp_{1, j} = 1

inline void solve()
{
  int n; cin >> n;
  vector dp(n + 1, vector<int>(10));
  for (int i = 0; i < 10; i ++) dp[1][i] = 1;
  for (int i = 1; i <= n; i ++)
  {
    for (int j = 1; j < 10; j ++)
    {
      for (int k = max(1LL, j - 1); k <= min(9LL, j + 1); k ++)
      {
        (dp[i][j] += dp[i - 1][k]) %= mod;
      }
    }
  }
  int ans = 0;
  for (int j = 1; j < 10; j ++) (ans += dp[n][j]) %= mod;
  cout << ans << "\n";
}


D.(记忆化搜索,二叉树) ABC-Transform

给定字符串 S 及其转换定义,问转换 t 次后第 k 个字符是什么

观察可知,第 {t, k} 个字母总是由 {t - 1, \lfloor \frac{k}{2} \rfloor} 变换而来

(字符串从0开始)发生如下变换

0 \rightarrow{1, 2} ,

1\rightarrow{2, 3} ,

2\rightarrow{4, 5} ...


不难发现这是一个类似二叉树的递归,我们能够在二叉树中自顶向下遍历,但是 t\in[0, 10^{18}] ,naively遍历下去肯定会T,我们做法是当 \lfloor \frac{k}{2} \rfloor 为0时,考虑第一位的变换,而实际上题目给定的 {A, B, C} 是三个字符为一个循环的,所以当 \lfloor \frac{k}{2} \rfloor = 0 是可以直接计算的,这也是为什么大家总是取 S_0 作为当 \lfloor \frac{k}{2} \rfloor = 0 的参照

inline void solve()
{
  string s; cin >> s;
  int q; cin >> q;

  function<int(int, int)> solve = [&](int t, int k)
  {
    if (t == 0) return 1LL * (s[k] - 'A');
    if (k == 0) return 1LL * (s[0] - 'A' + t) % 3;
    return (solve(t - 1, k / 2) + k % 2 + 1) % 3;
  };

  for (int i = 0; i < q; i ++)
  {
    int t, k; cin >> t >> k;
    cout << char('A' + solve(t, k - 1)) << "\n";
  }
}


E.(进制转换,思维,字符串) (∀x∀)

给定字符串 S ,问有多少个回文串 X 字典序小于或等于 S ?

26 进制考虑过于繁琐,所以先我们考虑十进制下的回文串, N 是偶数的话,我们直接翻转然后 insert 到原串后方

如: N = 6

001 \rightarrow001100

在问到 \leq3456 有多少个回文串时,显然为两数 {34, 56} 中较小数,字符串 X 中最大字典序的为 3443 ,总共有 35 个字符串 [0000, 0110, ..., 3443]

然后我们来考虑 N 为奇数的情况只需考虑 \frac{N+1}{2} 的翻转,与 N 为偶数同理,还是考虑翻转过去以后较小左半对称串的个数,先看前 \frac{N+1}{2} 位,仍然小于则成立

最后我们只需考虑更为繁琐的26进制:

仍然先找出左半对称串,然后拼接回文串,进制转换后判断大小,如果还是小于,就把 X 也加入计数

inline void solve()
{
  int n; cin >> n;
  string s; cin >> s;
  string t = s;
  string left = t.substr(0, (n + 1) / 2), right = left;
  reverse(right.begin(), right.end());
  if (n & 1) right.erase(right.begin());
  int cnt = 0;
  for (char ch : left) cnt = (cnt * 26 + ch - 'A') % mod;
  if (string(left + right) <= s) (cnt += 1) %= mod;
  cout << cnt << "\n";
}

F.(组合计数, 容斥定律, 递推, 思维) Black-and-White-Rooks

给定 N\times M 的棋盘,放入 B 个黑车和 W 个白车,两车互相不能攻击,方案数为多少 ?

首先不能互相攻击的两种车,他们的行列肯定是互不包含的,那么我们作图,先从理想方案(一般问题特殊化)假设黑白两车只分布在一块,考虑黑车占了 row_{B}col_{B} 列,白车占了 row_{W}col_{W} 列,同时假设黑车内部的方案数是 f_{row_B, col_B} 白车内部的方案数是 g_{row_W, col_W} 乘起来(相互关联乘法原理)后还要考虑讲 NM 列分配给对应 {row_B,row_W, col_B, col_W} 的方案数, N 行中选取 {row_B,row_W} 无序为组合,即 \tbinom{N}{N - row_B - row_W}

对应列同理得 \tbinom{M}{M - col_B - col_W} ,即 f_{row_B, col_B}g_{row_W, col_W}\times\tbinom{N}{N - row_B - row_W}\times\ \tbinom{M}{M - col_B - col_W}

如图:

这题太难,基本都是参考代码源杜老师的视频和《组合数学引论》,请大家多多支持代码源和namomo-camp


再考虑重复情况,黑白车分别占对应行列,但是当某行某列没有车时就会算重(未满行列)

接下来我们要解决的就是该问题的子问题了:即对应 {B, W} 区域每行每列都至少有一个车

递推式: f_{row_B, col_B} = \tbinom{row_B, col_B}{B} - \sum_{i, j}f_{i, j}\tbinom{row_B}{i}\tbinom{col_B}{j}

最后统计答案即可(后面看了下Rank前十的做法,大家好像都是这么做的)

inline void solve()
{
  int n, m, b, w; cin >> n >> m >> b >> w;
  int tot = n * m;
  vector<Z> fact(tot + 1), infact(tot + 1);
  fact[0] = 1;
  for (int i = 1; i <= tot; i ++) fact[i] = fact[i - 1] * i;
  infact[tot] = fact[tot].inv();
  for (int i = tot; i > 0; -- i) infact[i - 1] = infact[i] * i;
  auto C = [&](int x, int y)
  {
    return (x >= y && y >= 0 ? fact[x] * infact[y] * infact[x - y] : Z(0));
  };

  Z ans = 0;
  vector fb(n + 1, vector<Z>(m + 1)), gw(fb);
  for (int i = 0; i <= n; i ++)
    for (int j = 0; j <= m; j ++)
      for (int row = 0; row <= i; row ++)
        for (int col = 0; col <= j; col ++)
        {
          fb[i][j] += ((i - row + j - col) & 1 ? -1 : 1) * C(i, row) * C(j, col) * C(row * col, b);
          gw[i][j] += ((i - row + j - col) & 1 ? -1 : 1) * C(i, row) * C(j, col) * C(row * col, w);
        }
  
  for (int i = 0; i <= n; i ++)
    for (int j = 0; j <= m; j ++)
      for (int row = 0; row <= i; row ++)
        for (int col = 0; col <= j; col ++)
          ans += fb[row][col] * gw[i - row][j - col] * C(i, row) * C(j, col) * C(n, i) * C(m, j);
          
  cout << ans.val() << "\n";
}

G.(数据结构) Range-Pairing-Query

在已给定的颜色序列中查询每次 [l, r] 中最多出现的颜色

先插入一条类似题链接: F - Range Set Query (查询每次 [l, r] 中不同颜色的数目)

莫队做法:挺裸的,莫队就是用来解决这类查询问题的

离线+差分树状数组:也是和莫队一样的离线方式(将每次查询的答案记录, offline 计算每次以最多出现的颜色为基准, query_ir 排序,其他低于他次数的颜色不作贡献,每次 query_i 中标记最后一次出现最多的颜色,并 count (不过有一说一,题解那个代码跑得真的慢)

主席树:不太会...

inline void solve()
{
  int n, q; cin >> n;
  vector<int> a(n);
  for (int &x : a) cin >> x, x --;
  cin >> q;
  vector<int> l(q), r(q);
  for (int i = 0; i < q; i ++)
  {
    cin >> l[i] >> r[i];
    l[i] --;
  }
  vector<int> fa(q);
  iota(fa.begin(), fa.end(), 0);
  const int B = max(1.0, n / sqrt(q));
  sort(fa.begin(), fa.end(), [&](int x, int y)
  {
    if (l[x] / B == l[y] / B) return r[x] < r[y];
    return l[x] < l[y];
  });
  vector<int> cnt(n);
  int L = 0, R = 0, res = 0;
  auto add = [&](int x, int c)
  {
    res -= cnt[x] / 2;
    cnt[x] += c;
    res += cnt[x] / 2;
  };
  vector<int> ans(q);
  for (int x : fa)
  {
    while (L > l[x]) add(a[-- L], 1);
    while (R < r[x]) add(a[R ++], 1);
    while (L < l[x]) add(a[L ++], -1);
    while (R > r[x]) add(a[-- R], -1);
    ans[x] = res;
  }
  for (int x : ans) cout << x << "\n";
}

jls的板子又简洁跑得又快Orz

Ex.(Min-Max容斥) Random-Painting

N 个白色方格和一个装有 M 个球的盒子,重复给定操作直到所有方格变黑,求结束后的期望

操作为:

1.‎随机均匀地从盒子里捡起一个球

2.让 x 作为球的下标,将 L_x, L_{x + 1}, ..., R_x 方格涂黑

3.将球放回盒子

保证期望是有理数,此外在问题约束中定义互素的整数 P, Q 来表示一个值 \frac{P}{Q} ,可证有且仅有一个整数 R 满足 R\times Q\equiv P(mod \ 998244353) ,你需要找到整数 R

恶补数学ing

编辑于 2022-03-30 17:11