2025/11/17~2025/11/23 做题笔记

2025/11/17

C. 区间

发现修改操作是 + 1,考虑如何维护这个比较简单的修改。可以想到他会对一些区间的贡献 + 1,使得 \(k\) 的答案也增加。不难发现对于会产生影响的 \(k\),它产生的贡献先上升,然后不变,最后再下降。直接用线段树维护即可。

2025/11/18

A. 某道题的checker

纯飞舞吧,这题都不会。显然对于一个合法的 \(x\) 使得 \(a_i < a_{i + 1}\),当且仅当需要的那一位 \(x\)\(0\),并且前面的不同的位 \(x\) 都为 \(1\),那么剩下的没有限制的地方 \(x\) 随便填,所以高位前缀和对所有 \(x\) 算一下他能让多少 \(a_i < a_{i + 1}\) 满足。

主要是想到可以知道满足条件的 \(x\) 都是在一个最小的合法 \(x\) 上增加很多的 \(1\)

而我一直在想满足条件的 \(x\) 会是很多区间,但是当有很多相同的位的时候这个是不可维护的。之前一直不知道高为前缀和是个什么东西,现在了解了。

Code
#include <iostream>
#include <vector>

using namespace std;
using pii = pair<int, int>;

const int kN = 5e6 + 1;

int n, m, f[kN], ans[kN];

void Insert(int x, int y) {
  for (int i = m, s = 0; i >= 0; i--) {
    int o = x >> i & 1, _o = y >> i & 1;
    if (o == _o)
      continue;
    if (!o)
      f[s]++, f[s | (1 << i)]--;
    s |= 1 << i;
  }
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("in", "r", stdin);
  freopen("out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m;
  for (int i = 1, x, lst = -1; i <= n; i++) {
    cin >> x;
    if (lst != -1)
      Insert(lst, x);
    lst = x;
  }
  for (int j = 0; j < m; j++) {
    for (int i = 1; i < 1 << m; i++) {
      if (i & (1 << j))
        f[i] += f[i ^ (1 << j)];
    }
  }
  for (int i = 0; i < 1 << m; i++)
    ans[f[i]]++;
  for (int i = 0; i < n; i++)
    cout << ans[i] << ' ';
  return 0;
}

B. dfsize

这 dp 设计和转移纯巧妙吧。首先对于一个位置 \(i\),之后的 \(a_i\) 个位置都是 \(i\) 的子树。显然有 \(n^3\) 的区间 dp,设 \(f_{l, r}\) 表示 \([l, r]\) 中最少需要的改变次数。转移直接枚举位置把两边拼起来即可。考虑如何优化。首先可以用 \(f_{l, r}\) 表示区间 \([l, r]\) 变成森林的最少改变次数,这样新加一个最左边的点的时候变成树的代价就为 \([a_{l - 1} == r - l] + f_{l, r}\)

考虑如何转移。把整个区间变成一棵树是简单的,直接把最左边的点的值改为整个区间的大小即可,即 \(f_{l, r} = [a_l != r - l + 1] + f_{l + 1, r}\)。如果要把区间改成森林,发现最左边的点仍然是一颗树的根,完全可以把别的树的点也接到最左边的点上。只有一种情况不优,那就是最左边的点的值不用变就是一棵树的根,这种情况下是好转移的,即 \(f_{l, r} = f_{l, l + a_l - 1} + f_{l + a_l, r}\)

但是按照一般的枚举区间长度转移会很慢,因为访问的数组内存不是连续的。发现倒着枚举即可解决这个问题。

这道题的想法简直是纯天才。

Code
#include <iostream>

using namespace std;
using ll = long long;

const int kN = 5e3 + 2;

int T, n, a[kN];
ll ans, f[kN][kN];

int main() {
#ifndef ONLINE_JUDGE
  freopen("in", "r", stdin);
  freopen("out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> T; T--; ans = 0) {
    cin >> n;
    for (int i = 1; i <= n; i++)
      cin >> a[i];
    auto Calc = [](int l, int r) { return f[l + 1][r] + (a[l] != r - l + 1); };
    for (int l = n; l >= 1; l--) {
      for (int r = l; r <= n; r++) {
        int len = r - l + 1;
        f[l][r] = 1e9;
        if (a[l] <= len)
          f[l][r] = f[l][l + a[l] - 1] + f[l + a[l]][r];
        f[l][r] = min(f[l][r], Calc(l, r));
      }
    }
    for (int i = 1; i <= n; i++)
      for (int j = i; j <= n; j++)
        ans += a[i] ^ a[j] ^ Calc(i, j);
    cout << ans << '\n';
  }
  return 0;
}

C. 构造倒水

\(A\) 表示 \(a\) 中不是空瓶的数量, \(B\) 表示 \(b\) 中不是空瓶的数量

神仙题

首先先从 \(b = 1\) 的构造想。可以先蒙一个全部塞到 \(n\) 再一个一个往前倒的方案。再考虑有没有更优的方法。思考这个方案会在什么情况下比较劣,发现只取决于最初有多少个地方是有值的,所以思考当这个数大于 \(\dfrac{n}{3}\) 的时候怎么做。所以需要 \(n - A\) 这种东西。发现将每个有值的地方倒到 1 再倒到其它地方恰好是这 \(2(n - A)\),可以通过 \(b = 1\)

剩下的部分根本想不到一点。大致是说考虑全变成 1 后再满足目标是简单的,这两种方法都会多一个 \(n - B\) 的额外操作。但是这样不足以解决所有询问。还有一种可行的操作方式是先全部倒倒 \(n\),再倒到 \(b_i\) 的位置上再倒到 \(i\),这样的操作数是 \(A + 2B\),可以注意到对于一种序列,与前面两种方案最劣情况加起来为 \(5n\),也就是说他们三个的平均数恰为 \(\dfrac{5}{3}n\),这样同时应用三种方案即可得到至少一种方案满足条件。

Code
#include <cassert>
#include <iostream>
#include <vector>

using namespace std;
using pii = pair<int, int>;

const int kN = 5e5 + 1;

int T, n, k, a[kN], b[kN], A[kN], B[kN];
vector<pii> ans, res;

int main() {
#ifndef ONLINE_JUDGE
  freopen("in", "r", stdin);
  freopen("out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> T; T--;) {
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
      cin >> A[i];
    for (int i = 1; i <= n; i++)
      cin >> B[i];
    auto Fuck = []() {
      fill(a + 1, a + n + 1, 1);
      for (int i = 1, j = 1; i <= n; i++) {
        if (b[i])
          continue;
        for (; j <= n && a[j] >= b[j]; j++);
        // cerr << i << ' ' << j << '\n';
        res.push_back({i, j}), a[j]++;
      }
    };
    copy(A + 1, A + n + 1, a + 1), copy(B + 1, B + n + 1, b + 1), res.clear();
    for (int i = 1; i < n; i++)
      if (a[i])
        res.push_back({i, n});
    for (int i = n; i > 1; i--)
      res.push_back({i, i - 1});
    Fuck();
    swap(ans, res);

    copy(A + 1, A + n + 1, a + 1), copy(B + 1, B + n + 1, b + 1), res.clear();
    for (int i = 2, j = 1; i <= n; i++) {
      if (a[i])
        continue;
      if (a[1] >= 1) {
        res.push_back({1, i}), a[1]--, a[i]++;
        continue;
      }
      for (; a[j] <= 1 && j <= n; j++);
      if (j != 1)
        res.push_back({j, 1});
      res.push_back({1, i});
      a[j]--, a[i]++;
    }
    for (int i = 2; i <= n; i++)
      if (a[i] > 1)
        res.push_back({i, 1}), a[i]--, a[1]++;
    for (int i = 1; i <= n; i++)
      assert(a[i] == 1);
    Fuck();
    (res.size() < ans.size()) && (swap(ans, res), 0);
    
    copy(A + 1, A + n + 1, a + 1), copy(B + 1, B + n + 1, b + 1), res.clear();
    for (int i = 1; i < n; i++)
      if (a[i])
        res.push_back({i, n});
    for (int i = n - 1; i >= 1; i--) {
      if (b[i]) {
        res.push_back({n, b[i]});
        if (b[i] != i)
          res.push_back({b[i], i});
      }
    }
    (res.size() < ans.size()) && (swap(ans, res), 0);
  
    cout << ans.size() << '\n';
    for (auto [x, y] : ans)
      cout << x << ' ' << y << '\n', assert(x != y);
  }
  return 0;
}

2025/11/21

A. 探测

其实感觉这道题思维难度不高,但是我写代码的时候思维很乱,导致我 T1 写了十万年,差一点把正解交上去

可以发现如果特殊点不在 \(x\) 的子树内时,\(x\) 子树内的限制全部移到 \(x\) 后一定是相同的,那么可以借此往父亲转移,否则直接标记答案在此子树内。如果没有限制不同的点,那就随便走都行。如果直接把根设为一个限制的话会省去很多麻烦

Code
#include <algorithm>
#include <iostream>
#include <map>
#include <vector>

using namespace std;
using pii = pair<int, int>;

const int kN = 5e5 + 1, kI = 2e9;

vector<int> ans;
vector<pii> e[kN];
map<int, int> li[kN];
int T, n, rt, k, a[kN];

int Push(int x, int fa) {
  int mn = kI, mx = -kI;
  if (a[x] != -kI)
    mn = mx = a[x];
  for (auto [i, w] : e[x]) {
    if (i == fa)
      continue;
    li[x][i] = Push(i, x);
    if (li[x][i] != -kI)
      li[x][i] -= w, mn = min(mn, li[x][i]), mx = max(mx, li[x][i]);
  }
  if ((mn != mx && mn != kI && mx != -kI) || (mn != a[x] && a[x] != -kI) || mx > 1e9 || mn <= 0)
    return kI;
  if (mn != kI)
    a[x] = mn;
  return a[x];
}
void Find(int x, int fa, int dist) {
  // cerr << x << ' ' << fa << ' ' << dist << '\n';
  if (dist == 0)
    return ans.push_back(x);
  for (auto [i, w] : e[x])
    if (i != fa && li[x][i] > 1e9)
      return Find(i, x, dist - w);
  map<int, int> mp;
  if (a[x] != -kI)
    mp[a[x]]++;
  for (auto [i, w] : li[x])
    if (w != -kI)
      mp[w]++;
  if (mp.size() == 0 || mp.begin()->first == dist) {
    for (auto [i, w] : e[x])
      if (i != fa && li[x][i] == -kI)
        Find(i, x, dist - w);
  } else if (mp.size() == 1) {
    for (auto [i, w] : e[x])
      if (i != fa && li[x][i] != -kI)
        Find(i, x, dist - w);
  } else {
    for (auto [i, w] : e[x])
      if (i != fa && li[x][i] == mp.begin()->first)
        Find(i, x, dist - w);
  }
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("in", "r", stdin);
  freopen("out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> T; T--;) {
    cin >> n >> k;
    fill(a + 1, a + n + 1, -kI);
    for (int i = 1, x, y, z; i < n; i++) {
      cin >> x >> y >> z, e[x].push_back({y, z}), e[y].push_back({x, z});
    }
    if (k == 0) {
      cout << n << '\n';
      for (int i = 1; i <= n; i++)
        cout << i << ' ';
      continue;
    }
    for (int i = 1, x, y; i <= k; i++)
      cin >> x >> y, a[rt = x] = y;
    Push(rt, 0);
    Find(rt, 0, a[rt]);
    cout << ans.size() << '\n';
    sort(ans.begin(), ans.end());
    for (int i : ans)
      cout << i << ' ';
    cout << '\n';
    for (int i = 1; i <= n; i++)
      li[i].clear(), e[i].clear(), ans.clear();
  }
  return 0;
}

C. 有向稀疏图上更快的三元环计数

之前没改这道题是感觉太难了,根本想不明白。虽然现在还是感觉很难

有一个小 trick 是对于每一个异色三角形一定有两个异色角,这样限制只有两条边而不必像求同色三角形一样限制为三条边。需要注意到对于一对相交的边 \(i, j\),能选择的第三条边只有 \(\min(i, j)\) 个,统计答案的时候对于 \(i < j\)\(i >= j\) 分别计算贡献即可。

Code
#include <iostream>

using namespace std;

const int kN = 5e6 + 2, kM = 998244353;

int n, ans, a[3][kN], f[3][kN][2], s[3][kN][2];
char ch;

int main() {
#ifndef ONLINE_JUDGE
  freopen("in", "r", stdin);
  freopen("out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int o : {0, 1, 2}) {
    for (int i = 1; i <= n; i++) {
      cin >> ch, a[o][i] = ch - '0';
      for (int t : {0, 1})
        s[o][i][t] = s[o][i - 1][t] + (a[o][i] == t), f[o][i][t] = (f[o][i - 1][t] + (a[o][i] == t) * i) % kM;
    }
  }
  for (int o = 0; o < 2; o++) {
    for (int _o = o + 1; _o < 3; _o++) {
      for (int i = 1; i <= n; i++) {
        int t = !a[_o][i];
        ans = (ans + 1ll * (s[o][n][t] - s[o][max(i, n - i - 1)][t]) * i) % kM;
        if (i >= n - i)
          ans = (ans + f[o][i][t] - f[o][max(0, n - i - 1)][t]) % kM;
      }
    }
  }
  ans = 1ll * -ans * (kM + 1) / 2 % kM + kM;
  for (int i = 1; i <= n; i++)
    ans = (ans + 1ll * i * (i + 1) / 2 % kM) % kM;
  for (int i = 0; i <= n; i += 2)
    ans = (ans + 1ll * (n - i) * (n - i - 1) / 2 % kM) % kM;
  cout << ans << '\n';
  return 0;
}

2025/11/22

D. 序列求交

观察部分分发现 \(q = 0\) 的占比很大,思考没有修改怎么做。可以发现对于每一个在 \(A\) 中的元素能产生贡献的区间左端点范围是 \([max(1, i - k + 1), min(n - k + 1, i)]\),同理,在 \(B\) 序列中能产生贡献的也是这个东西。发现每一个 \(v\) 元素能产生贡献的点对构成一个矩形,那么对于这个矩形内部全部 + 1,扫描线一下即可知道答案。

对于有修改的部分,发现每个修改即为平移两个矩形,所以只会对四个 \(1 \times len\) 的矩形修改,类似可持久化线段树的区间修改操作做即可。这个东西我一开始还不会,只能说 SP11470 TTM - To the moon 全忘了

Code
#include <iostream>
#include <vector>

using namespace std;
using pii = pair<int, int>;
using ll = long long;

const int kN = 1e5 + 2;

int n, k, q, cnt, a[kN], rt[kN];
pii b[kN], c[kN];
struct Info {
  ll mx, cnt;

  Info operator+(const Info& x) const {
    int _mx = max(mx, x.mx);
    return {_mx, (_mx == mx) * cnt + (_mx == x.mx) * x.cnt};
  }
};
namespace Seg {
Info tr[kN * 4];

void Update(int p, Info v, int x = 1, int l = 1, int r = n) {
  if (l == r)
    return tr[x] = v, void();
  int m = (l + r) / 2;
  if (p <= m)
    Update(p, v, x * 2, l, m);
  else
    Update(p, v, x * 2 + 1, m + 1, r);
  tr[x] = tr[x * 2] + tr[x * 2 + 1];
}
}  // namespace Seg

struct Line {
  int l, r, v;
};
vector<Line> li[kN];
struct Tr {
  Info v;
  int l, r, tag;
} tr[kN * 300];

void Build(int x, int l, int r) {
  tr[x].v.cnt = r - l + 1;
  if (l == r)
    return;
  int m = (l + r) / 2;
  Build(tr[x].l = ++cnt, l, m), Build(tr[x].r = ++cnt, m + 1, r);
}
void Add(int y, int x, int nl, int nr, int v, int l = 1, int r = n) {
  tr[x] = tr[y];
  if (nl <= l && r <= nr)
    return tr[x].v.mx += v, tr[x].tag += v, void();
  int m = (l + r) / 2, L = tr[y].l, R = tr[y].r;
  if (nl <= m)
    Add(L, tr[x].l = ++cnt, nl, nr, v, l, m);
  if (nr > m)
    Add(R, tr[x].r = ++cnt, nl, nr, v, m + 1, r);
  tr[x].v = tr[tr[x].l].v + tr[tr[x].r].v, tr[x].v.mx += tr[x].tag;
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("in", "r", stdin);
  freopen("out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> k >> q;
  for (int i = 1; i <= n; i++) 
    cin >> a[i], c[a[i]] = {max(i - k + 1, 1), min(i, n - k + 1)};
  for (int i = 1, x; i <= n; i++) {
    cin >> x, b[x] = {max(i - k + 1, 1), min(i, n - k + 1)};
    li[c[x].first].push_back({b[x].first, b[x].second, 1}), li[c[x].second + 1].push_back({b[x].first, b[x].second, -1});
  }
  Build(rt[0], 1, n);
  auto Update = [](int p, int l, int r, int v, int tmp = 0) { tmp = rt[p], Add(tmp, rt[p] = ++cnt, l, r, v); };
  for (int i = 1; i <= n; i++) {
    Add(rt[i - 1], rt[i] = ++cnt, 1, n, 0);
    for (auto [l, r, v] : li[i])
      Update(i, l, r, v);
    Seg::Update(i, tr[rt[i]].v);
  }
  cout << Seg::tr[1].mx << ' ' << Seg::tr[1].cnt << '\n';
  for (int p; q--;) {
    cin >> p;
    Update(c[a[p]].first, b[a[p]].first, b[a[p]].second, -1), Update(c[a[p + 1]].second, b[a[p + 1]].first, b[a[p + 1]].second, -1);
    swap(a[p], a[p + 1]), swap(c[a[p]], c[a[p + 1]]);
    Update(c[a[p]].first, b[a[p]].first, b[a[p]].second, 1), Update(c[a[p + 1]].second, b[a[p + 1]].first, b[a[p + 1]].second, 1);
    if (c[a[p]].second == n - k + 1)
      Update(n - k + 1, b[a[p]].first, b[a[p]].second, 1), Update(n - k + 1, b[a[p + 1]].first, b[a[p + 1]].second, -1);
    if (c[a[p + 1]].first == 1)
      Update(1, b[a[p + 1]].first, b[a[p + 1]].second, 1), Update(1, b[a[p]].first, b[a[p]].second, -1);
    Seg::Update(c[a[p]].first, tr[rt[c[a[p]].first]].v), Seg::Update(c[a[p + 1]].second, tr[rt[c[a[p + 1]].second]].v);
    cout << Seg::tr[1].mx << ' ' << Seg::tr[1].cnt << '\n';
  }
  return 0;
}

A. 天和地

乍一看有点唬人,但是仔细想想就会发现最后的图会形成一个内向基环树,而其它不能被 \(1\) 访问的点指向哪里都无所谓,直接枚举 \(1\) 能到多少点算一下即可

Code
#include <iostream>

using namespace std;

const int kM = 998244353;

int n, ans;

int Pow(int x, int y) {
  int res = 1;
  for (; y; y >>= 1, x = 1ll * x * x % kM)
    (y & 1) && (res = 1ll * res * x % kM);
  return res;
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("in", "r", stdin);
  freopen("out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n, ans = Pow(n, n - 1);
  for (int i = 2, base = n - 1; i <= n; i++) {
    ans = (ans + 1ll * i * i % kM * base % kM * Pow(n, n - i) % kM) % kM;
    base = 1ll * base * (n - i) % kM;
  }
  cout << 1ll * ans * Pow(Pow(n, n), kM - 2) % kM;
  return 0;
}

C. 椅子与篮筐

感觉如果放 \(O(n\log n)\) 过的话比 T2 简单。

看到这题首先可以想到一个二分答案,但是看到数据范围直接开摆了。但是正解也是从 \(n \log n\) 优化的,实在是有点唐。

二分答案是显然可以的,考虑 \(Check\) 如何做。很明显不能让他走到任意一个 \(a_x \geq v\) 的位置,考虑 \(f_{x}\) 表示进入这颗子树前必须要做多少次操作才能不猜到这样的位置。显然状态转移方程即为 \(f_x = max(\sum\limits_{y \in son}f_y - 1, 0) + [a_x \geq v]\),这样常熟可以写的很小。我把他给的快读加上后程序快了一倍

Code
#include <iostream>

using namespace std;

const int kN = 2e7 + 1;

namespace io {

char inputbuf[1 << 23], *p1 = inputbuf, *p2 = inputbuf;
#define getchar() (p1 == p2 && (p2 = (p1 = inputbuf) + fread(inputbuf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline int read() {
  int ret = 0;
  char ch = getchar();
  bool f = true;
  for (; ch < '0' || ch > '9'; ch = getchar())
    if (ch == '-')
      f = false;
  for (; ch >= '0' && ch <= '9'; ch = getchar())
    ret = ret * 10 + (ch ^ 48);
  return f ? ret : -ret;
}

}  // namespace io

int n, l, r, a[kN], f[kN], fa[kN];

bool Check(int t) {
  fill(f + 1, f + n + 1, 0);
  for (int i = n; i >= 1; i--) {
    f[i] = max(f[i] - 1, 0) + (a[i] >= t);
    f[fa[i]] += f[i];
  }
  return f[1];
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("in", "r", stdin);
  freopen("out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  n = io::read(), l = 0, r = n - 1;
  for (int i = 2; i <= n; i++)
    fa[i] = io::read();
  for (int i = 2; i <= n; i++)
    a[i] = io::read();
  for (int m = (l + r + 1) / 2; l < r; m = (l + r + 1) / 2) {
    if (Check(m))
      l = m;
    else
      r = m - 1;
  }
  cout << l;
  return 0;
}
posted @ 2025-11-17 11:15  sb-yyds  阅读(12)  评论(0)    收藏  举报