csp2024赛前集训

2024-09-22

开题顺序:ABDC

时间分配:A:20min,B:30min,C:1.5h,D:30min,其余时间打摆。

主观难度:绿/1600,蓝/1900,紫/2400,蓝/2100

set

fi,j 表示前 i 个数和为 j 的方案数,然后直接 01 背包,最后用快速幂把每种和的数量次方乘起来就行了。由于 f 最后要当指数,所以要 mod(kM1)

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 205, kM = 998244353;

LL f[kMaxN * kMaxN], n, ans = 1;

LL P(LL x, LL y) {
  LL ans = 1;
  for (LL i = 1; i <= y; i <<= 1, x = x * x % kM) {
    (y & i) && (ans = ans * x % kM);
  }
  return ans;
}

int main() {
  freopen("set.in", "r", stdin);
  freopen("set.out", "w", stdout);
  ios :: sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  f[0] = 1;
  for (int i = 1; i <= n; i++) {
    for (int j = n * n; j >= i; j--) {
      f[j] = (f[j] + f[j - i]) % (kM - 1);
    }
  }
  for (int i = 1; i <= n * n; i++) {
    f[i] && (ans = ans * P(i, f[i]) % kM);
  }
  cout << ans;
  return 0;
}

hire

设当前 i 位置上的人有 ai 个,考虑到在合法时一定会有一些性质:

l,r[1,n],lr,i=lrai(rl+1+d)×k

将其拆开:

i=lrair×kl×k+k+d×k

i=lr(aik)k×d

然后直接用线段树维护每个位置的权值为 aik 的序列的全局最大子段和就行。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 5e5 + 5;

struct T {
  LL s, l, r, ans;

  T() {
    s = l = r = ans = 0;
  }
} w[kMaxN << 2];

LL n, m, k, d;

T Merge(T x, T y) {
  T v;
  v.s = x.s + y.s;
  v.l = max(x.l, x.s + y.l);
  v.r = max(y.r, x.r + y.s);
  v.ans = max({x.ans, y.ans, x.r + y.l});
  return v;
}

void Update(int u, int l, int r, int x, LL y) {
  if (l == r && l == x) {
    w[u].s += y;
    w[u].l = w[u].r = w[u].ans = max(0LL, w[u].s);
  } else if (l > x || r < x) {
    return;
  } else {
    int mid = l + r >> 1;
    Update(u << 1, l, mid, x, y);
    Update(u << 1 | 1, mid + 1, r, x, y);
    w[u] = Merge(w[u << 1], w[u << 1 | 1]);
  }
}

T Query(int u, int l, int r, int L, int R) {
  if (l > R || r < L) {
    T v;
    v.s = v.l = v.r = v.ans = 0;
    return v;
  } else if (L <= l && r <= R) {
    return w[u];
  } else {
    int mid = l + r >> 1;
    return Merge(Query(u << 1, l, mid, L, R), Query(u << 1 | 1, mid + 1, r, L, R));
  }
}

int main() {
  freopen("hire.in", "r", stdin);
  freopen("hire.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m >> k >> d;
  for (int i = 1; i <= n; i++) {
    Update(1, 1, n, i, -k);
  }
  for (LL x, y; m; m--) {
    cin >> x >> y;
    Update(1, 1, n, x, y);
    cout << (Query(1, 1, n, 1, n).ans <= k * d ? "YES" : "NO") << "\n";
  }
  return 0;
}

block

这个题状态设计本质和 P6773 的状态设计一样,但赛时并没有看出来。

fi,j 表示以 i 为根的子树中选择了的连通块中有限制的点的 dfn 最大值为 j (如果不存在有限制的点在连通块内 j 为 0),转移找到前一个子树内 dfn 最大的点,然后与当前子树内的 dfn 最大的有限制点判断是否被限制到,没有就算上。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;
using Pii = pair<int, int>;

const int kMaxN = 1e5 + 5, kMaxM = 55;
const LL kInf = 1e18;

LL f[kMaxN][kMaxM], a[kMaxN], vis[kMaxN], p[kMaxN], id[kMaxN], c, ans, n, m;
vector<int> g[kMaxN];
set<Pii> s;

void Dfs(int u, int fa) {
  f[u][p[u]] = a[u];
  for (int i : g[u]) {
    if (i != fa) {
      Dfs(i, u);
      LL val = -kInf;
      for (int j = 0; j <= c; j++) {
        !s.count({i, id[j]}) && (val = max(val, f[u][j]));
      }
      for (int j = 0; j <= c; j++) {
        f[u][j] = max(f[u][j], f[i][j] + val);
      }
    }
  }
  for (int i = 0; i <= c; i++) {
    ans = max(ans, f[u][i]);
  }
}

int main() {
  freopen("block.in", "r", stdin);
  freopen("block.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
  }
  for (int i = 1, x, y; i <= n; i++) {
    for (cin >> x; x; x--) {
      cin >> y;
      g[i].push_back(y), g[y].push_back(i);
    }
  }
  fill(f[0], f[n + 1], -kInf);
  for (int i = 1, x, y; i <= m; i++) {
    cin >> x >> y;
    vis[x] = vis[y] = 1;
    s.insert({x, y}), s.insert({y, x});
  }
  for (int i = 1; i <= n; i++) {
    vis[i] && (p[i] = ++c, id[c] = i);
  }
  Dfs(1, 0);
  cout << ans;
  return 0;
}

checkers

一开始看见是个数数题感觉是神仙题不太敢做,但看完题好像和 set 所用的大体思想差不多(?

首先考虑没有 ? 时怎么做。设字符串中有 x0,有 y11(比如 '001110' 就有 3 个 0 和 1 个 '11',最后的那个 '1' 不算),那么方案数就是 (x+yx)。那么就可以用 fi,j,k,0/1,0/1 表示前 i 个字符有 j0k11,当前这一位是 0/1,这一段 1 的数量有偶数/奇数个。那么就有转移:

  1. 当前这一位题目给出的字符不是 1

fi,j,k,0,0=fi1,j1,k,0,0+fi1,j1,k,1,0+fi1,j1,k,1,1

  1. 当前这一位题目给出的字符不是 0

fi,j,k,1,0=fi1,j,k1,1,1

fi,j,k,1,1=fi1,j,k,0,0+fi1,j,k,1,0

这样时间复杂度是 O(n3),带 7 倍常数,不过可以加一些减枝:ji,2×k+ji,最后剪完好像是 12 倍常数的。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 505, kMaxS = 1e3 + 5, kM = 1e9 + 7;

LL f[2][kMaxN][kMaxN][2][2], p[kMaxS], inv[kMaxS], n, ans, op = 1;
// f[i][j][k][0/1][0/1]:前i位有j个0,k对连续的1,当前选0/1,前面有连续偶/奇个1
string s;

LL P(LL x, LL y) {
  LL ans = 1;
  for (LL i = 1; i <= y; i <<= 1, x = x * x % kM) {
    (y & i) && (ans = ans * x % kM);
  }
  return ans;
}

void DpS(int i) {  // 当前选0
  for (int j = 1; j <= i; j++) {
    for (int k = 0; k * 2 + j <= i; k++) {
      f[op][j][k][0][0] += f[op ^ 1][j - 1][k][0][0];
      f[op][j][k][0][0] += f[op ^ 1][j - 1][k][1][0];
      f[op][j][k][0][0] += f[op ^ 1][j - 1][k][1][1];
      f[op][j][k][0][0] %= kM;
    }
  }
}

void DpT(int i) {  // 当前选1
  for (int j = 0; j <= i; j++) {
    for (int k = 0; k * 2 + j <= i; k++) {
      k && (f[op][j][k][1][0] = f[op ^ 1][j][k - 1][1][1]);
      f[op][j][k][1][1] = (f[op ^ 1][j][k][0][0] + f[op ^ 1][j][k][1][0]) % kM;
    }
  }
}

int main() {
  freopen("checkers.in", "r", stdin);
  freopen("checkers.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> s;
  s = ' ' + s;
  f[0][0][0][0][0] = 1;
  for (int i = 1; i <= n; i++) {
    if (s[i] == '0') {
      DpS(i);
    } else if (s[i] == '1') {
      DpT(i);
    } else {
      DpS(i), DpT(i);
    }
    op ^= 1;
    for (int j = 0; j <= i; j++) {
      for (int k = 0; k * 2 + j <= i; k++) {
        f[op][j][k][0][0] = f[op][j][k][1][0] = f[op][j][k][1][1] = 0;
      }
    }
  }
  p[0] = inv[0] = 1;
  for (int i = 1; i <= n * 2; i++) {
    p[i] = p[i - 1] * i % kM;
    inv[i] = P(p[i], kM - 2);
  }
  for (int i = 0; i <= n; i++) {
    for (int j = 0; j <= n; j++) {
      for (int k = 0; k < 2; k++) {
        for (int l = 0; l < 2; l++) {
          ans = (ans + f[n & 1][i][j][k][l] * p[i + j] % kM * inv[i] % kM * inv[j] % kM) % kM;
        }
      }
    }
  }
  cout << ans;
  return 0;
}

2024-09-24

开题顺序:BCAD

时间分配:A 1h,B 1.5h,C 1h,D 0.5h

难度评估:蓝/1700,蓝/2100,紫/2600,蓝/2200

yiyi

非常抽象的博弈论。

首先把 b=c=0 的情况判掉,这时一定是 Second

那么不难发现,剩下的情况下 a 都只会和先后手顺序有关。考虑到一个人操作序列一定会类似于 1,1,2,1,2,1,2,1,2,1,2,2,1,2,1,2,1,2,,所以直接求出最多操作多少轮,然后按照轮数奇偶性判段即可。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

LL t, a, b, c;

int main() {
  freopen("yiyi.in", "r", stdin);
  freopen("yiyi.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  for (cin >> t; t; t--) {
    cin >> a >> b >> c;
    if (!b && !c) {
      cout << "Second\n";
    } else {
      a %= 2;
      bool op = ((1 + (b > c + 1)) % 2 ? c * 2 + 1 : b * 2 - 2) % 2;
      bool _op = ((1 + (c > b + 1)) % 2 ? b * 2 + 1 : c * 2 - 2) % 2;
      if ((b && (op + a) % 2) || (c && (a + _op) % 2)) {
        cout << "First\n";
      } else {
        cout << "Second\n";
      }
    }
  }
  return 0;
}

wuyi

算是比较简单的期望(?,不过赛时因为过于感性的化简式子浪费了一车时间。

枚举第一个 i>ai 的位置。令 ai=j,k=ni+1,那么前 i 个数的填数方案如下:

j=1i1(k+1)ij1×kj

其中 kj 是因为 1j 都只能填 k 个(对于一个位置 l,其能填 ln,而其中有 il+1 个数已经被用了),(k+1)ij1类似。

然后大力化简:

j=1i1(k+1)ij1×kj=kk+1×(k+1)nkj=0nk1(kk+1)j

用等比数列求和公式化简:

=k×[(k+1)nknk]

然后再乘上后面那些位置的填法,每个位置贡献如下:

(nk+1)×k!×[(k+1)nkknk]

然后加上 val(p)=n+1 时的情况(即所有 ai=i),于是有最终的期望式子:

ans=k=1n(nk+1)×k!×[(k+1)nkknk]+n+1n!

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 1e6 + 5, kM = 998244353;

LL p[kMaxN], f[kMaxN], n, ans;

LL P(LL x, LL y) {
  LL ans = 1;
  for (LL i = 1; i <= y; i <<= 1, x = x * x % kM) {
    (y & i) && (ans = ans * x % kM);
  }
  return ans;
}

int main() {
  freopen("wuyi.in", "r", stdin);
  freopen("wuyi.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  p[0] = 1;
  for (int i = 1; i <= n; i++) {
    p[i] = p[i - 1] * i % kM;
  }
  for (int i = 1; i <= n; i++) {
    ans = (ans + (n - i + 1) * p[i] % kM * (P(i + 1, n - i) - P(i, n - i) + kM) % kM) % kM;
  }
  cout << (ans + n + 1) % kM * P(p[n], kM - 2) % kM;
  return 0;
}

yijiu

赛时因为斐波那契数列预处理项数少了挂了 20pts /fn。

结论:对一个数进行唯一分解的方法从大到小找到最大的那小于当前数并减去所得到的方案一定是不相邻的。

非常经典的结论,在百度随便搜一下应该都有证明。

那么令 fi 为第 i 项的斐波那契数列,si=si1si2fi2fi,当 fi2 为偶数的时候还要异或上 fi1

然后对与一个区间 [l,r] 求答案。令 L 为最小满足 lfL 的数, R 为最大的 fRr 的数,那么 [l,r] 的答案就是 [l,fL1] 的答案异或上 [fR+1,r] 的答案再异或上 sRsLfl(至于为什么是这个式子可以把 s 展开进行证明)。

然后考虑边界条件,此时没有合法的 L,R,所以找到最大的 x 使得 fx<l,然后递归求 [lfx,rfx] 的答案,如果 (rl+1) 是奇数那么还要额外异或上 fx。由于在第 87 个斐波那契数就大于 1018,所以时间复杂度大概是 O(nlogn) 的。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 105;
const LL kInf = 1e18;

LL f[kMaxN], s[kMaxN], cnt = 87, l, r, t;

LL Solve(LL l, LL r) {
  if (l > r) {
    return 0;
  }
  int pl = lower_bound(f + 1, f + cnt + 1, l) - f;
  int pr = upper_bound(f + 1, f + cnt + 1, r) - f - 1;
  if (pl > pr) {
    LL v = (r - l + 1) % 2 * f[pr];
    return Solve(l - f[pr], r - f[pr]) ^ v;
  } else {
    return Solve(l, f[pl] - 1) ^ Solve(f[pr] + 1, r) ^ s[pr] ^ s[pl] ^ f[pl];
  }
}

int main() {
  freopen("yijiu.in", "r", stdin);
  freopen("yijiu.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  f[1] = 1, f[2] = 2;
  s[1] = 1, s[2] = 3;
  for (int i = 3; i <= cnt; i++) {
    f[i] = f[i - 1] + f[i - 2];
    s[i] = s[i - 1] ^ s[i - 2] ^ f[i - 2] ^ f[i];
    (f[i - 2] % 2 == 0) && (s[i] ^= f[i - 1]);
  }
  for (cin >> t; t; t--) {
    cin >> l >> r;
    cout << Solve(l, r) << "\n";
  }
  return 0;
}

pear

赛时看出来了可能是个同余最短路板子,但考虑到复杂度最坏可能会被卡到 O(nmlogm),于是没有写。然而正解复杂度是基于随机生成方式的!!!!期望是 O(mlogm) 的。强烈谴责!!!!

直接求出 a 中的最小值,然后以最小值为模数做同余最短路,然后直接就做完了。

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 1e7 + 5;

struct T {
  LL id, x;

  bool operator<(const T &a) const {
    return x > a.x;
  }
};

LL a[kMaxN], dis[kMaxN * 3], v, n, m, seed;
bool vis[kMaxN * 3];
priority_queue<T> q;

void Dijkstra() {
  memset(dis, 0x3f, sizeof(dis));
  for (q.push({0, dis[0] = 0}); q.size();) {
    int x = q.top().id;
    q.pop();
    if (!vis[x]) {
      vis[x] = 1;
      for (int i = 1; i <= n; i++) {
        int nxt = (x + a[i]) % v;
        if (dis[nxt] > dis[x] + a[i]) {
          q.push({nxt, dis[nxt] = dis[x] + a[i]});
        }
      }
    }
  }
}

int main() {
  freopen("pear.in", "r", stdin);
  freopen("pear.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  v = 2e9;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    v = min(v, a[i]);
  }
  Dijkstra();
  LL ans = 0;
  for (int i = 0; i < v; i++) {
    ans = max(ans, dis[i] - v);
  }
  cout << ans;
  return 0;
}

2024-09-25

开题顺序:ABCD

时间分配:A 30min, B 1h, C 2.5h, D <1min

难度评估:绿/1500,绿/1500,紫/2500,紫/2500(大模拟素质还是有点过于高了。)

change

非常正常的签到题。

很显然的想法是设 fi,j,0/1i 个数,改了 j 次,当前这一位改/不改,所能得到的最大值。

但是这样不好转移,所以重新设计状态。 fi,j,0,1 表示前 i 个数,改了 j 次,当前这一位是/不是山谷点,所能得到的最大值。

可以得到转移:

fi,j,0=maxfi1,j,0,fi1,j,1

考虑当前这一位是山谷点,如果当前这一位改,那么前一位改一定不优,有:

fi,j,1=fi1,j1,0+min(ai11,ai,ai+11)

如果不改,那么必须要 ai1>aiai+1>ai,有:

fi,j,1=fi1,j,0+ai

最后答案就是 max(fn,i,0)

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 2e3 + 5;

LL f[kMaxN][kMaxN][2], a[kMaxN], n, k, ans;  // f[i][j][0/1]: 第i位前面修改了j次,当前这一位是/不是

int main() {
  freopen("change.in", "r", stdin);
  freopen("change.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> k;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
  }
  memset(f, -0x3f, sizeof(f));
  f[1][0][0] = 0;
  for (int i = 2; i <= n; i++) {
    for (int j = 0; j <= k; j++) {
      // 不是:
      f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j][1]);
      // 是:
      // 1.不改:
      (a[i - 1] > a[i] && a[i + 1] > a[i]) && (f[i][j][1] = f[i - 1][j][0] + a[i]);
      // 2.改:
      j && (f[i][j][1] = max(f[i][j][1], f[i - 1][j - 1][0] + min({a[i], a[i - 1] - 1, a[i + 1] - 1})));
    }
  }
  for (int i = 0; i <= k; i++) {
    ans = max(ans, f[n][i][0]);
  }
  cout << ans;
  return 0;
}

alternate

比较签到的找规律题。

首先进行一个手玩:

ai=ai+ai+1

ai=aiai+1=aiai+2

ai=ai+ai+1=aiai+2+ai+1ai+3

ai=aiai+1=ai2×ai+2+ai+4

ai′′′′′=ai+ai+1=ai2×ai+2+ai+4+ai+12×ai+3+ai+5

ai′′′′′′=ai′′′′′ai+1′′′′′=ai3×ai+2+3×ai+4ai+6

不难发现,这个东西操作 i 次后的值的系数就是杨辉三角,也就是一个组合数,可以直接用乘法逆元做到 O(nlogn)

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 1e5 + 5, kM = 1e9 + 7;

LL a[kMaxN], p[kMaxN], inv[kMaxN], n;

LL P(LL x, LL y) {
  LL ans = 1;
  for (LL i = 1; i <= y; i <<= 1, x = x * x % kM) {
    (y & i) && (ans = ans * x % kM);
  }
  return ans;
}

LL C(LL x, LL y) {
  if (x < 0 || y < 0 || x < y) {
    return 0;
  } else {
    return p[x] * inv[y] % kM * inv[x - y] % kM;
  }
}

LL Solve(int p) {
  LL ans = 0;
  for (int i = p, c = 0; i <= n; i += 2, c++) {
    int op = (c & 1) ? -1 : 1;
    ans = (ans + op * C((n - 1) / 2, c) * a[i] % kM + kM) % kM;
  }
  return ans;
}

int main() {
  freopen("alternate.in", "r", stdin);
  freopen("alternate.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  p[0] = inv[0] = 1;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    p[i] = p[i - 1] * i % kM;
    inv[i] = P(p[i], kM - 2);
  }
  if (n == 1) {
    cout << a[1];
    return 0;
  }
  if (n & 1) {
    cout << Solve(1);
  } else {
    cout << (Solve(1) + Solve(2)) % kM;
  }
  return 0;
}

box

第一次在赛事想出这种比较有难度的数数,然后因为细节问题调了几乎一整场没调出来。

首先考虑 LIS 这个限制该怎么处理。考虑用另一种方式求 LIS。按照值域从小到大一次插入序列中的指定位置,对于每个数,因为其比前面所有数都大,所以以其为结尾的 LIS 长度就是前面的 LIS 长度加 1。

然后考虑每次在比赛中遇到的人都是已经收买了的人这个限制怎么处理。我们可以把比赛关系看成一棵树,那么树上每一个点的权值就是这个子树内的最大值(默认 1 比所有被收买的人大)。那么从根节点到 1 所在叶子节点的路径上的所有儿子的权值要么是 1 要么是被收买的,先钦定这个链全都向左儿子走,如图:

假如已经知道了所有红色的点的权值,那么剩下的位置填数的方案数很好算,按照权值从小到大,假设当前权值为 v,权值比其小的点已经有了 x 个,当前填的是第 l 层,所以其贡献是 (v2x2l11)×(2l1)!

于是可以直接开始 dp,设 fi,S,T 表示已经填完了 1 到 i,已经填了的位置的集合状压后为 j,LIS 构成的位置的集合状压后为 k。考虑加入 ai+1 有哪些选法:

  1. 不选 ai+1

那么有:

fi+1,j,k=fi,j,k

  1. ai+1

假设选择将 ai+1 放在了第 l 层这个位置,插入之后的 LIS 的集合为 nxt,那么有:

fi+1,j|2l1,nxt=fi,j,k×(ai+12j2l11)×(2l1)!

最后 1 在第 1 个位置的方案数就是 i,kpopcount(i)fm,2n1,i

然后考虑 1 不在第一个位置。在二叉树上的一个链的每一个岔路都可以交换左右两个子树,所以将和乘上 2n 即可。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 19, kMaxM = 19, kMaxS = 1 << 10;

int a[kMaxM], s[kMaxS], n, m, k, kM;
LL f[kMaxM][kMaxS][kMaxS], p2[kMaxS * 10], p[kMaxS * 10], inv[kMaxS * 10], ans;
// f[i][s][t]:a[1]~a[i],选了树上的位置集合为s,LIS集合为t包含于 s
// nxt[i][j]:i加入j后LIS的集合
// p:阶乘,inv:阶乘逆元,p2:2^i

LL P(LL x, LL y) {
  LL ans = 1;
  for (LL i = 1; i <= y; i <<= 1, x = x * x % kM) {
    (y & i) && (ans = ans * x % kM);
  }
  return ans;
}

LL C(LL x, LL y) {
  if (x < 0 || y < 0 || x < y) {
    return 0;
  } else {
    return p[x] * inv[y] % kM * inv[x - y] % kM;
  }
}

int main() {
  freopen("box.in", "r", stdin);
  freopen("box.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m >> k >> kM;
  p[0] = inv[0] = p2[0] = 1;
  for (int i = 1; i < kMaxS * 10; i++) {
    p[i] = p[i - 1] * i % kM;
    p2[i] = p2[i - 1] * 2 % kM;
    inv[i] = P(p[i], kM - 2);
  }
  for (int i = 1; i <= m; i++) {
    cin >> a[i];
  }
  sort(a + 1, a + m + 1);
  for (LL i = 1; i < 1ll << n; i++) {
    int l = i & (-i);
    s[i] = s[i - l] + 1;
  }
  f[0][0][0] = 1;
  for (int i = 0; i < m; i++) {
    for (int j = 0; j < 1 << n; j++) {
      for (int k = j; 1; k = (k - 1) & j) {
        f[i + 1][j][k] = (f[i + 1][j][k] + f[i][j][k]) % kM;
        for (int l = 1; l <= n; l++) {  // 在第l个位置放入 a[i + 1],
          if (!(j & (1 << l - 1))) {
            int nxt = (k >> (l - 1) ? (k >> (l - 1) & (k >> (l - 1)) - 1) << (l - 1) | k & (1 << (l - 1)) - 1 : k);
            f[i + 1][j | (1 << l - 1)][nxt | (1 << l - 1)] = (f[i + 1][j | (1 << l - 1)][nxt | (1 << l - 1)] + f[i][j][k] * C(a[i + 1] - 2 - j, p2[l - 1] - 1) % kM * p[p2[l - 1]] % kM) % kM;
            // 子树内需要:(2^(l-1))-1
            // 可供选择:a[i+1]-2-j
          }
        }
        if (!k) {
          break;
        }
      }
    }
  }
  for (int i = 0; i < 1 << n; i++) {
    if (s[i] >= k) {
      ans = (ans + f[m][(1 << n) - 1][i]) % kM;
    }
  }
  cout << ans * p2[n] % kM;
  return 0;
}

poker

纸张大模拟,找时间再写。

2024-09-26

开题顺序:ABDC

时间分配:A 1h,B 30min, C 1h, D 1.5h

难度评估:绿/1800,蓝/2000,蓝/1900,紫/2400

difference

抽象贪心+构造题,赛时因为一个小错误挂了 90。

首先要根据 k 进行分类讨论,一共有三种情况:

  1. k>0

如果最大值为 k,那么找到一个不为 0 也不等于 k 的数放到第一个,剩下的数从大到小排序。否则直接从大到小排序。

  1. k=0

根据鸽巢原理,当出现次数最多的数的出现次数大于 n2 时是无解的。否则把出现次数最多的数放在奇数位置,然后剩下的数随便放。

  1. k<0

k>0 差不多,只不过最后从小到大排即可。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;
using Pii = pair<int, int>;

const int kMaxN = 2e6 + 5;

struct T {
  int x, y;

  bool operator<(const T &a) const {
    return y < a.y;
  }
};

int a[kMaxN], b[kMaxN], n, k, t;

void Calc() {
  cin >> n >> k;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
  }
  sort(a + 1, a + n + 1);
  if (k > 0) {
    if (a[n] == k) {
      int pos = 0;
      for (int i = 1; i <= n; i++) {
        (a[i] != k && a[i]) && (pos = i);
      }
      if (!pos) {
        cout << "no\n";
        return;
      } else {
        cout << "yes\n";
        cout << a[pos] << " ";
        int c = 0;
        for (int i = 1; i <= n; i++) {
          (pos != i) && (b[++c] = a[i]);
        }
        sort(b + 1, b + c + 1, greater<int>());
        for (int i = 1; i <= c; i++) {
          cout << b[i] << " ";
        }
        cout << "\n";
      }
    } else {
      cout << "yes\n";
      sort(a + 1, a + n + 1, greater<int>());
      for (int i = 1; i <= n; i++) {
        cout << a[i] << " ";
      }
      cout << "\n";
    }
  } else if (!k) {
    priority_queue<T> q;
    map<int, int> mp;
    for (int i = 1; i <= n; i++) {
      mp[a[i]]++;
    }
    for (Pii i : mp) {
      q.push({i.first, i.second});
    }
    for (int i = 1; i <= n; i++) {
      T x = q.top();
      q.pop();
      if (x.x != a[i - 1]) {
        a[i] = x.x;
        if (x.y > 1) {
          q.push({x.x, x.y - 1});
        }
      } else {
        if (!q.size()) {
          cout << "no\n";
          return;
        }
        T y = q.top();
        q.pop();
        a[i] = y.x;
        if (y.y > 1) {
          q.push({y.x, y.y - 1});
        }
        q.push(x);
      }
    }
    for (int i = 1; i <= n; i++) {
      if (a[i - 1] == a[i]) {
        cout << "no\n";
        return;
      }
    }
    cout << "yes\n";
    for (int i = 1; i <= n; i++) {
      cout << a[i] << " ";
    }
    cout << "\n";
  } else {
    if (a[1] == k) {
      int pos = 0;
      for (int i = 1; i <= n; i++) {
        (a[i] != k && a[i]) && (pos = i);
      }
      if (!pos) {
        cout << "no\n";
        return;
      } else {
        cout << "yes\n";
        cout << a[pos] << " ";
        int c = 0;
        for (int i = 1; i <= n; i++) {
          (i != pos) && (b[++c] = a[i]);
        }
        sort(b + 1, b + c + 1);
        for (int i = 1; i <= c; i++) {
          cout << b[i] << " ";
        }
        cout << "\n";
      }
    } else {
      sort(a + 1, a + n + 1);
      cout << "yes\n";
      for (int i = 1; i <= n; i++) {
        cout << a[i] << " ";
      }
      cout << "\n";
    }
  }
}

void Init() {
}

int main() {
  freopen("difference.in", "r", stdin);
  freopen("difference.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  for (cin >> t; t; t--) {
    Calc();
    Init();
  }
  return 0;
}

tour

一个合格的签到题。

首先路径长度显然可以提出来单独处理,枚举每条边计算贡献即可,于是只要考虑计算每条路径上的权值的最大值之和即可。

考虑枚举最大值是多少,然后把值小于等于当前值的点进行标记。然后枚举每个权值等于当前枚举的最大值的点,计算有多少条路径使得路径上每个点都被标记且经过了当前点。可以通过子树大小计算。时间复杂度 O(n2)

考虑进行优化。在枚举最大值的时候用并查集维护当前已经被标记的点构成的联通块。每次加入一个点可以合并若干连通块,用当前最大值乘上当前合并的两个连通块的大小的乘积即可。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 1e6 + 5, kM = 1e9 + 7;

LL a[kMaxN], fa[kMaxN], s[kMaxN], b[kMaxN], f[kMaxN], sz[kMaxN], tot, ans, n;
int vis[kMaxN];
vector<int> g[kMaxN], p[kMaxN];

int Find(int x) {
  return fa[x] == x ? x : fa[x] = Find(fa[x]);
}

void Dfs(int u, int fa) {
  sz[u] = 1;
  for (int i : g[u]) {
    if (i != fa) {
      Dfs(i, u);
      sz[u] += sz[i];
    }
  }
  ans = (ans - 1LL * sz[u] * (n - sz[u]) % kM + kM) % kM;
}

int main() {
  freopen("tour.in", "r", stdin);
  freopen("tour.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    fa[i] = i, s[i] = 1;
    b[++tot] = a[i];
  }
  for (int i = 1, u, v; i < n; i++) {
    cin >> u >> v;
    g[u].push_back(v), g[v].push_back(u);
  }
  Dfs(1, 0);
  sort(b + 1, b + tot + 1);
  tot = unique(b + 1, b + tot + 1) - b - 1;
  for (int i = 1; i <= n; i++) {
    a[i] = lower_bound(b + 1, b + tot + 1, a[i]) - b;
    p[a[i]].push_back(i);
  }
  for (int i = 1; i <= tot; i++) {
    for (int j : p[i]) {
      vis[j] = 1;
      for (int k : g[j]) {
        if (vis[k]) {
          ans = (ans + 1LL * b[i] * s[Find(k)] % kM * s[Find(j)] % kM) % kM;
          s[Find(j)] += s[Find(k)];
          fa[Find(k)] = Find(j);
        }
      }
    }
  }
  cout << ans * 2 % kM;
  return 0;
}

permutation

感觉 <<<<<<<<< T2,然而很多人貌似因为这个题的位置原因没写。

考虑 k 很小,所以可以直接暴力枚举所有合法的字符串。设 fi,j 表示 in 的位置,第 i 个填的是字符 j,所能得到的最大代价。转移枚举 i+1 填的是什么,大力转移即可。

然后从前往后按照字典序从大到小枚举每一位填啥,如果前缀的值加上后缀求出的 f 值小于 x 就不进行搜索。搜到第 k 个合法的串输出即可。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 1e3 + 5;
const LL kInf = 1e15;
const string op = "NOIP", rid = "INOP";

LL f[kMaxN][4], v[5][5], n, x, k;
// f[i][j]:前 i 个位置,选了字典序第0/1/2/3,最大代价是多少
// 字典序:INOP
string s;
char ans[kMaxN];
map<char, int> id;

void Dfs(int p, LL pval) {
  if (p > n) {
    if (pval >= x) {
      k--;
      if (!k) {
        for (int i = 1; i <= n; i++) {
          cout << ans[i];
        }
        cout << "\n";
        exit(0);
      }
    }
    return;
  } else {
    if (s[p] == '?') {
      for (int i = 3; i >= 0; i--) {
        LL val = f[p][i];
        (p > 1) && (val += v[id[ans[p - 1]]][i]);
        if (val + pval >= x) {
          LL nxt = pval;
          (p > 1) && (nxt += v[id[ans[p - 1]]][i]);
          ans[p] = rid[i];
          Dfs(p + 1, nxt);
        }
      }
    } else {
      int i = id[s[p]];
      ans[p] = s[p];
      LL val = f[p][i];
      (p > 1) && (val += v[id[ans[p - 1]]][i]);
      if (val + pval >= x) {
        LL nxt = pval;
        (p > 1) && (nxt += v[id[ans[p - 1]]][i]);
        ans[p] = rid[i];
        Dfs(p + 1, nxt);
      }
    }
  }
}

int main() {
  freopen("permutation.in", "r", stdin);
  freopen("permutation.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  id['I'] = 0, id['N'] = 1, id['O'] = 2, id['P'] = 3;
  cin >> n >> x >> k >> s;
  s = ' ' + s;
  for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 4; j++) {
      cin >> v[id[op[i]]][id[op[j]]];
    }
  }
  fill(f[0], f[kMaxN], -kInf);
  if (s[n] == '?') {
    f[n][0] = f[n][1] = f[n][2] = f[n][3] = 0;
  } else {
    f[n][id[s[n]]] = 0;
  }
  for (int i = n - 1; i >= 1; i--) {
    if (s[i] == '?') {
      for (int j = 0; j < 4; j++) {
        for (int k = 0; k < 4; k++) {
          f[i][j] = max(f[i][j], f[i + 1][k] + v[j][k]);
        }
      }
    } else {
      for (int j = 0; j < 4; j++) {
        f[i][id[s[i]]] = max(f[i][id[s[i]]], f[i + 1][j] + v[id[s[i]]][j]);
      }
    }
  }
  Dfs(1, 0);
  cout << "-1";
  return 0;
}

border

CSP-S 模拟赛考 SAM 板子,素质不高。

考虑 border 的性质。对于一个子串,其某段前缀和后缀是相同的。而这两个相同的字符串可以唯一确定一个子串。所以题目转化为每种字符串的代价是其长度乘以其出现次数,然后求每种字符串的代价之和。

每个子串的长度和出现次数可以用 SAM 求出。建出 SAM 的 DAG 图,某个位置的子串的出现次数就是这个点可以到达的没有出度的点的数量。对于 DAG 图上的每一个点,根据 SAM 的性质,其结尾位置的集合相等的字符串长度是连续的。所以用每个点的所有字符串的长度之和乘上出现次数即可。

由于设在 DAG 图上目前在考虑 u 节点,其前继节点为 fa。由于前继节点包含了部分 u 节点的字符串,所以要扣除。设 lenx 表示点 x 中的字符串的最长的长度,sx 表示 x 能到达的没有出度的点的个数。那么每个串可以任意选出两个位置,所以方案数是 (su2),所以点 u 所对应的字符串的所有代价和为:

(su2)×(lenu+lenfa+1)×(lenulenfa)2

对每个点计算代价求和即可。

code:

#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int kMaxN = 2e5 + 5;

struct A {
  LL pre, len;
  int a[26];

  A() {
    pre = len = 0;
    memset(a, 0, sizeof(a));
  }
} w[kMaxN];

LL sz[kMaxN], lst, c, n;
LL ans;
string s;
vector<int> g[kMaxN];

void Init() {
  w[++c].len = 0, w[c].pre = 0;
  lst = c;
}

void Insert(char ch) {
  int x = ch - 'a', u = ++c;
  sz[u] = 1;
  int pos = lst;
  w[u].len = w[lst].len + 1;
  for (; pos && !w[pos].a[x]; pos = w[pos].pre) {
    w[pos].a[x] = u;
  }
  if (pos) {
    int v = w[pos].a[x];
    if (w[v].len == w[pos].len + 1) {
      w[u].pre = v;
    } else {
      int cl = ++c;
      w[cl].len = w[pos].len + 1, w[cl].pre = w[v].pre;
      for (int i = 0; i < 26; i++) {
        w[cl].a[i] = w[v].a[i];
      }
      for (; pos && w[pos].a[x] == v; pos = w[pos].pre) {
        w[pos].a[x] = cl;
      }
      w[u].pre = w[v].pre = cl;
    }
  } else {
    w[u].pre = 1;
  }
  lst = u;
}

void Dfs(int u, int fa) {
  for (int i : g[u]) {
    Dfs(i, u);
    sz[u] += sz[i];
  }
  ans += 1LL * (w[u].len + w[fa].len + 1) * (w[u].len - w[fa].len) / 2 * sz[u] * (sz[u] - 1) / 2;
}

int main() {
  freopen("border.in", "r", stdin);
  freopen("border.out", "w", stdout);
  ios ::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> s;
  s = ' ' + s;
  Init();
  for (int i = 1; i <= n; i++) {
    Insert(s[i]);
  }
  for (int i = 1; i <= c; i++) {
    if (w[i].pre >= 1) {
      g[w[i].pre].push_back(i);
    }
  }
  Dfs(1, 0);
  cout << ans;
  return 0;
}

2024-10-01

Beautiful Bracket Sequence (hard version)

一个序列的深度是子序列的最大值,所以考虑如何去确定这个最大值。

假设没有 ?,可以设 fi=j=1sj=(j=1sj=),不难发现 f0<0fn>0,而 i0n 一定是在一直加一,所以找到 fi=0i 就是最佳决策点。所以如果序列中一共有 y 个右括号,那么 一定有 fy=0。所以只需要统计 [y+1,n] 这个序列中的右括号个数即可得到该序列的深度。

所以我们可以在右括号和左括号之间统计贡献。对于 si= ?,其只有在选择右括号时会产生贡献,并且所有右括号的数量之和要小于当前位置。如果一共有 x 个问号,那么方案数为 j=0iy2(x1j)。同理,如果一个位置为右括号,则贡献为 j=0iy1(xj),这两个东西可以前缀和处理,总时间复杂度 O(n)

code

Move by Prime

首先考虑对于一个序列怎么计算。考虑到每次操作只能乘上或初除以一个质数,而最后如果数相等那么每个质数的出现次数一定相等。所以对于每个质数分开考虑,显然需要将每种质数的个数变成所有的该质数的出现次数的中位数。

那么对于一个序列,令质数 p 的出现次数的中位数为 vp,代价为:

pi,p|xi|cntp,ivp|

由于 cnt 数组是固定的,所以考虑每个数在系数为 1 和系数为 1 时分别会作为子序列中的一个数贡献多少次,即可计算出结果。

于是考虑贡献该如何计算,对于当前计算的一个定值 pp 作为因子在这 n 个数中的出现次数分别为 c1,c2,c3cn。对 c 从小到大排序那么第 i 个位置在所有子序列中系数的贡献就是:

j=in1(n1j)j=0i2(n1j)

其中 j 是质因数 p 出现次数比 ci 大的数选了的个数加上 p 的出现次数比左边小的数的数中没有出现的数。

最终枚举 p,对所有 c 排序,暴力算即可。不过有一些细节,比如说 ci=0 的数要特殊考虑一下。

总时间复杂度 O(nlog2n),实际上根本跑不满。

code

Weak Subsequence

一道很不错的数数题。

结论一:如果存在一个长度为 x 的弱字串,那么一定存在一个长度为 x 的弱字串使得这个弱字串是由一个字符和一个长度为 x1 的字串组成的

考虑为什么是对的,观察下面一张图:(其中黑色是原串,红色是选出来的子序列,蓝色是子串)

那么如下这种方案显然是一样的:

所以该结论显然是正确的。

结论二:极长的弱子串的子序列和子串一定开头在整个串的开头要么结尾是整个串的结尾。

这个结论比较显然,如图:

那么往左延伸时显然子串和子序列每个位置的字符都是相同的,所以一定可以延伸到两端。

对于前缀的一段,那么最长弱子串就是最后面的从后往前第一个相同的两个字符中靠前的那个字符的位置加上一。后缀同理。

跟据鸽巢原理,最多在第 k+1 一个位置就会出现两个相同的字符。于是暴力枚举不同的位数,组合计算即可。

至于如何计算,可以容斥。用最长弱子串长度 w 的串的数量减去长度 w1 的串的数量。

剩下的直接分类讨论前面第一次相等的位置的前缀与后面第一次相等的位置的后缀是否重叠,直接计算即可。

code

posted @   caoshurui  阅读(93)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
点击右上角即可分享
微信分享提示