专栏文章

不懂

个人记录参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@mipj6kr2
此快照首次捕获于
2025/12/03 12:52
3 个月前
此快照最后确认于
2025/12/03 12:52
3 个月前
查看原文

04.21

最小生成树

考虑 krustal 最小生成树的过程,先把边排序,再做一个依次选边的过程。如果我们有了最后的排序结果,就可以轻易求出最小生成树的边权;如果没有的话,就应该想办法记录 kkaia_i 与求最小生成树边权和的某些中间量的关系。
先排序,如何限制每条边只能被贡献到一边呢?把反边记录下来,如果考虑到反边时 (u,v)(u, v) 仍未连通则此时必须选它,选是指计入对 mst 的贡献,因此若两条边都不被选也不影响。要保存的信息就是连通性了,题解告诉我们 Bell(9)Bell(9) 并不大,可以计入 dp 维度,另一个维度存当前 mst 几条边使用了 aia_i
我在场上一直在想贪心或性质,或反悔贪心。不过似乎连通性不是什么很强的条件,2m2^m 种选法中很多不优。似乎考虑到模拟 krustal 的排序后就自然而然了。本质似乎还是发现,确定好某一种选法后,得到答案的过程中,所依赖的信息(连通性)可以压缩,于是一边记录依赖的信息一边记录答案所在维度跑 dp。

操作

先套路性取 log\log 转为加法,那么初始有一个 0101 数组,我们要做模意义下的 0101 背包。
场上在试图优化 (xai)(1+xbi)(\sum x^{a_i}) \prod (1+x^{b_i}),很不可做。
先把问题转化为,把 [i+w,j+w][i+w, j+w] 或到 [i,j][i, j] 上面。有一个做法是注意到最多只有 pp0011,用 log\log 左右的复杂度找到下一个更新点是可接受的。可以用树状数组维护区间哈希,线段树修改,到完全一样时停止递归。势能是不一样的位置数,但 (0,1)(0, 1) 是无效的。不过注意到背包在模意义下,任何一个 (0,1)(0, 1) 必定对应一个 (1,0)(1, 0),就对了。
另一个做法是认为操作很快就会有循环节,所以按 gcd(p1,x)\gcd(p-1, x) 分组,每组内随机打乱,直到不可更新时 break。
前一个做法似乎又是可想的,但是敢去写二分下一个不一致还是需要勇气啊。或许又不那么可想,毕竟过的人也没那么多。

04.22

P8386

有人笨笨的,想先枚举最左的那个匹配 11 后合法的位置 ii,则 [1,i][1, i] 不合法 [i+1,n][i+1, n] 合法,就能递推了。
但这是错误的,因为 [1,i][1, i] 不合法时,后面接一段合法序列,是有可能合法的。
所以还是来看看怎么判定吧。设 fif_i 表示以 ii 结尾的部分是否合法,则 fi=aj=aifj1f_i = \cup_{a_j=a_i} f_{j-1},维护 S={aifi=1=1}S=\{a_i \mid f_{i=1}=1\} 即可,计数时,转移系数全部相同即可视为等价类,则只有 S|S| 是关心的,转移时维护 S|S|fif_i 即可。
判定改 dp 最重要的是压缩,即把对答案贡献相同的放进同一个等价类内一起转移,转移时,维护判定用的信息,以及到达终点时区分不同类的信息(这道题不需要)。也可以看成,任意一个合法解顺着唯一的路径到达计数终点(有点自动机的感觉了)。

桂花树

晚点再写。

04.23

离场切最近的一次。
操作相当于删点,最后的树形态为留下点的虚树,因此找到最大同时存在于两棵树内的树即可。
做一个 dp,fuf_u 表示以 uu 为根的最大子树大小。则转移时,枚举 uu 的儿子,这些儿子必须在两棵树内都位于 uu 的子树中,且它们两两没有祖先后代关系,可以跑最大权独立集。
记录一下最大权独立集的做法:
考虑基础的暴力,f(msk)f(msk) 表示要求这个集合内点的最大权独立集,找到最高位 xx,分 xx 是否被选向下继续搜索,当 msk<2n/2msk<2^{n/2} 时记录答案。
前面 n/2n/2 次拓展只会产生 2n/22^{n/2} 中状态,后面的 2n/22^{n/2} 种情况都会被记忆到。
CPP
std::function<int(LL)> dfs = [&](LL st) {
  if (st < lim && dp[st] != -1) return dp[st];
  if ((st & res) == st) return 0;
  int x = std::__lg(st);
  LL r = st & (all ^ ban[x]);
  int ans = std::max(dfs(st ^ (1ll << x)), dfs(r) + g[x]);
  if (st < lim) dp[st] = ans;
  return ans;
};
CF1336E1 证明复杂度的方法也是这样。
场上想了半天最大权独立集用网络流怎么做,哈哈。

序列

对于单点不保存修改,分类讨论强制不选和强制选就可以了。
那么设 fif_i 表示只使用 [1,i][1, i]gig_i 表示只使用 [i,n][i, n]hih_i 表示强制选 ii,答案可以 O(1)O(1) 得到。
fif_i 是比较好求的:fi=max{fj+(ji)(ji1)2C+sisj}f_i = \max\{ f_j+\frac{(j-i)(j-i-1)}{2} C + s_i-s_j\},拆开发现是以 j-j 为斜率的一次函数,可以拿来做斜率优化。
考虑 hih_i,它是一个 maxxiy{fx1+(yx)(yx+1)2C+sysx1+gy+1}\max_{x \leq i \leq y}\{ f_{x-1} + \frac{(y-x)(y-x+1)}{2} C + s_y - s_{x-1} + g_{y+1} \}
区间问题考虑分治,想办法把 [l,mid][l, mid](mid,r](mid, r] 分开考虑。拆,考虑 fif_i 表示 x=i,y(mid,r]x=i, y \in (mid, r] 的最大贡献,这也是一个斜率为 y-y 自变量为 xx 的一次函数,同样可以斜率优化对 i[l,mid]i \in [l, mid] 做,做完后拿来更新 [i,mid][i, mid],同样倒着做一次即可更新 (mid,y](mid, y]
考虑跨越 midmid(x,y)(x, y)(x,y)(x, y) 拿来更新区间中任何一个点时都不会比使用 (x,y)(x, y) 更劣,因此是正确的。
真没怎么做过这样的题,区间对内部所有点做贡献。不过可以理解的,f(x,y)[x,y]f(x, y) \to [x, y] 拆成 f(x,y)[x,mid]f(x, y) \to [x, mid]f(x,y)(mid,y]f(x, y) \to (mid, y],前者发现与 yy 无关,拆成 f(x,(mid,r])[x,mid]f(x, (mid, r]) \to [x, mid],后者发现与 xx 无关,拆成 f([l,mid],y)[mid,y]f([l, mid], y) \to [mid, y],又发现 f(x,(mid,r])f(x, (mid, r]) 右侧是定值,将 xx 看作变量思考。
有空再写,感觉没空。

04.24

图上的游戏

先拆贡献,把边的贡献拆到点上,发现是出度减入度,再乘一个可以自己挑选的系数。那么把点按出度减入度的绝对值排序,双方一定依次选最前面的。也可以发现确定数值的点的贡献一定为 00,因为所有方案对称。
dp,按 outin|\text{out}-\text{in}| 排序后跑背包,这是为了处理相等的部分。 把确定数值的点挑出来单独跑背包,合并时卷一起就行。
写不动了,理解了一下 hz 的代码。
CPP
#include <bits/stdc++.h>
using namespace std;

const int N = 55, mod = 1e9 + 7, iv2 = (mod + 1) / 2;
int n, m, c[N];
void add(auto &x, auto y) {x = (x + y) % mod; }
struct node{
  int ind, oud;
}a[N * N];
int tot;
int power(int x, int y) {
  int ret = 1;
  while (y) {
    if (y & 1) ret = 1ll * ret * x % mod;
    x = 1ll * x * x % mod, y >>= 1;
  }
  return ret;
}
int get_inv(int x) {return power(x, mod - 2); }

int fac[N], inv[N], C[N][N];
int p[N][N], f[2][N][N][N], g[2][N][N][N];
void init() {
  fac[0] = inv[0] = 1;
  for (int i=1; i<=m; ++i) fac[i] = 1ll * fac[i - 1] * i % mod;
  inv[m] = get_inv(fac[m]);
  for (int i=m-1; i; --i) inv[i] = 1ll * inv[i + 1] * (i + 1) % mod;
  for (int i=0; i<=n; ++i) {
    C[i][0] = 1;
    for (int j=1; j<=i; ++j)
      C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
  }
  p[0][0] = 1;
  for (int i=1; i<=n; ++i)
    for (int j=0; j<=m; ++j)
      for (int k=0; j+k<=m; ++k)
        add(p[i][j + k], 1ll * p[i - 1][j] * inv[k]);
  for (int i=0; i<=m; ++i)
    for (int j=0; j<=m; ++j) a[++tot] = (node) {i, j};
  sort(a + 1, a + tot + 1, [](node x, node y) {
    return abs(x.ind - x.oud) > abs(y.ind - y.oud);
  });
  g[0][0][0][0] = 1;
  for (int i=1; i<=tot; ++i) {
    int l = (i & 1), r = (l ^ 1);
    int mul = 1ll * inv[a[i].ind] * inv[a[i].oud] % mod;
    for (int k=0; k<=n; ++k)
      for (int in=0; in<=m; ++in)
        for (int out=0; out<=m; ++out) {
          if (!g[r][k][in][out]) continue;
          for (int x=0,pw=1; k+x<=n&&in+x*a[i].ind<=m&&out+x*a[i].oud<=m; ++x,pw=1ll*pw*mul%mod) {
            add(g[l][k + x][in + x * a[i].ind][out + x * a[i].oud], 1ll * pw * g[r][k][in][out] % mod * C[k + x][x]);
            add(f[l][k + x][in + x * a[i].ind][out + x * a[i].oud], 1ll * pw * f[r][k][in][out] % mod * C[k + x][x]);
            if (x & 1) {
              int tmp = std::abs(a[i].ind - a[i].oud);
              if ((k + x) % 2 == 0) tmp = mod - tmp;
              add(f[l][k + x][in + x * a[i].ind][out + x * a[i].oud], 1ll * pw * g[r][k][in][out] % mod * C[k + x][x] % mod * tmp);
            }
          }
          g[r][k][in][out] = f[r][k][in][out] = 0;
        }
  }
}

int main() {
  cin >> n >> m;
  for (int i=1; i<=n; ++i) cin >> c[i];
  init();
  for (int i=1,cnt=0; i<=n; ++i) {
    cnt += c[i] == 2;
    for (int j=1; j<=m; ++j) {
      //printf("calc %d %d\n", i, j);
      int ans = 0;
      for (int in=0; in<=j; ++in)
        for (int out=0; out<=j; ++out) {
          add(ans, 1ll * f[tot & 1][cnt][in][out] * p[i - cnt][j - in] % mod * p[i - cnt][j - out]);
          //printf("add in %d out %d f %d p %d * %d\n", in, out, f[tot & 1][cnt][in][out], p[i - cnt][j - in], p[i - cnt][j - out]);
        }
      ans = 1ll * ans * fac[j] % mod * fac[j] % mod;
      cout << 1ll * ans * iv2 % mod << " ";
    }
    cout << "\n";
  }
  return 0;
}

04.25

P11915

首先答案有单调性,可以二分也可以从高到低扫,一边扫一边排除不合法的 (u,v)(u, v),直到答案集合被清空。
那么假设希望 disddis \leq d,对于所有 dis>ddis > d 的点对 (i,j)(i, j),我们希望加上 (u,v)(u, v)dis(i,j)ddis(i, j) \leq d。我们断言,iuvji \to u \to v \to jivuji \to v \to u \to j 中至多一个合法,也就是,枚举 (u,v)(u, v)(i,j)(i, j),用 (i,j)(i, j) 排除不合法的 (u,v)(u, v),就能删掉所有不合法点对。
要省掉一维,对 vv 记录 mxv=max{dis(v,j)i,dis(i,j)>d}mx_v=\max\{dis(v, j) \mid \exist i, dis(i, j) > d \},直接比较 dis(u)+mxvdis(u) + mx_v,若合法则直接跳过当前 dd,否则弹掉 (u,v)(u, v),复杂度是均摊的立方。
感觉得紫。

CF1774F2

考虑到操作 33 相当于把当前序列克隆一遍并对克隆体减去全部操作 22 的值,再把当前累计要减的 tag 翻倍。由翻倍得知这种操作最多出现 log\log 次,相当于只用考虑前 log\log 次操作。
fif_i 表示进行第 ii 个三操作会造成多少伤害,注意到有 2fifi+12 f_i \leq f_{i+1},从大到小排序,若第 ii 个操作不选,则后面操作可以任选,故扫一遍即可,复杂度 O(nlogx)O(n \log x)

04.30

怎么感觉大家都什么都会。

这是第一题

从后往前倒推 sg 函数值,同行有障碍物是好做的,如果没有的话,注意到只能一直往一个方向走,且 sg 值不会超过 33,递推一下,如果走进来那个格子的下方是 xx,则继续往下走的 sg 值是 yy。这玩意其实有终点,就是走一步就要到当前格子的情况。
CPP
v3 f(n, v2(m, v1(3, -1)));
std::function<int(int, int, int)> dfs = [&](int u, int v, int op) {
  if (u >= n || a[u][v]) return 100;
  if (f[u][v][op] != -1) return f[u][v][op];
  int p = (v - 1 + m) % m;
  int s1 = op != 1 ? dfs(u, p, 0) : 100;
  p = (v + 1) % m;
  int s2 = op != 0 ? dfs(u, p, 1) : 100;
  int s3 = dfs(u + 1, v, 2);
  return f[u][v][op] = mex(s1, s2, s3);
};
就是考虑到,当 op 确定后,相当于是一个确定的数跟另一个要去 dfs 的值求 mex,把这视为函数复合,则做做前后缀和就可以了。
确实不可以回头走,所以,当手动确定方向之后,就最多只能走一圈了。不能回头反而让问题变简单了啊,你怎么搞反了。
经验好像是要把式子展开看看,或者看看你是不是想复杂了。就像事实上是二元而不是三元。
CPP
#include <bits/stdc++.h>

int mex(int a, int b = 3, int c = 3) {
  if (a != 0 && b != 0 && c != 0) return 0;
  else if (a != 1 && b != 1 && c != 1) return 1;
  else if (a != 2 && b != 2 && c != 2) return 2;
  else return 3;
}

struct tp {
  int a[4];
  tp() { for (int i = 0; i < 4; i++) a[i] = i; }
  tp operator + (const tp &t) const {
    tp res;
    for (int i = 0; i < 4; i++) res.a[i] = t.a[a[i]];
    return res;
  }
};

int main() {
  #ifndef LOCAL
  freopen("first.in", "r", stdin);
  freopen("first.out", "w", stdout);
  #endif
  int n, m; scanf("%d %d", &n, &m); 
  std::vector<std::vector<int>> c(n, std::vector<int>(m + 1));
  std::vector<std::pair<int, int>> p;
  for (int i = 0; i < n; i++) {
    for (int j = 1; j <= m; j++) {
      char t; scanf(" %c", &t);
      if (t == '#') c[i][j] = 1;
      else if (t == 'B') c[i][j] = 2;
    }
  }
  std::vector<std::vector<int>> f(n + 1, std::vector<int>(m + 1));
  f[n].assign(m + 1, 100);
  int ans = 0;
  for (int i = n - 1; i >= 0; i--) {
    std::vector<tp> a(m + 2), pl(m + 2), pr(m + 2), sl(m + 2), sr(m + 2);
    for (int j = 1; j <= m; j++)
      for (int k = 0; k < 4; k++)
        if (c[i][j] == 1) a[j].a[k] = 3;
        else a[j].a[k] = mex(f[i + 1][j], k);
        for (int j = 1; j <= m; j++) 
          pl[j] = a[j] + pl[j - 1], pr[j] = pr[j - 1] + a[j];
        for (int j = m; j >= 1; j--) 
          sl[j] = sl[j + 1] + a[j], sr[j] = a[j] + sr[j + 1];
        for (int j = 1; j <= m; j++) {
          if (c[i][j] == 1) f[i][j] = 3;
          else 
            f[i][j] = mex(f[i + 1][j], (pl[j - 1] + sl[j + 1]).a[3], (sr[j + 1] + pr[j - 1]).a[3]);
          if (c[i][j] == 2) ans ^= f[i][j];
        }
  }
  puts(ans ? "Alice" : "Bob");
}

loj2732

等价于求只保留 x\geq x 的数组成多少个连续段,等价于有多少对相邻的数 (a,b)(a, b) 一个在 xx 上一个在 xx 下。
拆贡献。若 xax \leq a 贡献 1-1,否则贡献 11,对 bb 也一样。于是只有当 axba \leq x \leq b 的时候会有 22 的贡献,容易用树状数组维护。

05.01

T1

我从下往上贪心做的。有一些奇怪的神秘维护划分树也过了呀。为什么别人那么能把自己不确定正确性的东西写出来呢。

T2

先把题意等价到,在 n×mn \times m 的矩阵中四连通游走,从 (x,y)(x', y') 出发,一共移动 tt' 次,且第 ttt'-t 次不能位于 (x,y)(x, y)。拆成从 (x,y)(x', y') 出发移动 tt' 次,从 (x,y)(x', y') 移动 ttt'-t 次到 (x,y)(x, y),从 (x,y)(x, y) 移动 tt 次,均不能触碰边界。
容易发现两维独立,求出来卷起来就好了。
那么怎么求从 xx 出发游走 tt 步不触碰上下边界的方案数呢,考虑经典的反射容斥(P3266),答案是一个 O(t/n)O(t/n) 项组合数加减。
枚举 nn,再考虑在 tt 移动时快速求出答案。那么注意到组合数的上下指标移动都是 O(1)O(1) 级别的,每次递推复杂度为 O(t2/n)O(t^2/n)
ntn \geq \sqrt{t} 时这样做,否则使用 O(nt)O(nt) 的朴素暴力,复杂度为 O(tt)O(t \sqrt t)

评论

0 条评论,欢迎与作者交流。

正在加载评论...