Loading

【做题笔记】2024暑假集训做题笔记

1. dp 专题

1.1 状压dp

P2396 yyy loves Maths VII

\(f_S\) 表示当集合状态为 \(S\) 时的方案数,\(dis_S\) 表示当集合状态为 \(S\) 时的总数。

预处理 \(dis_{2^i}=a_i\) 然后转移,当 \(dis_S\)\(b_i\) 时,则不可转移,否则 \(f[S]=\sum\limits_{i=1}^n[i\in S] \cdot f[S \bigoplus i]\)

发现每一次只需要 \(S\) 中的 \(1\) 的位置,每次 \(lowbit\) 跳到最低位的 \(1\) 即可。

时间复杂度 \(O(2^n n)\),稍微卡常+O2即可通过。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 1000000007
using namespace std;

const ll N = 24, M = (1<<N)+1;

int n, m, b[3], f[M];

ll dis[M];

inline int lb(int x) {
  return x & -x;
}

inline void solve(int S, int x) {
  for (register int k = x, nS = S; k; nS -= k, k = (nS & -nS)){
    f[S] = (f[S] + f[k ^ S] % mod) % mod;
  }
} 

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  for (int i = 0; i < n; i++) cin >> dis[(1 << i)];
  cin >> m;
  for (int i = 0; i < m; i++) cin >> b[i];
  f[0] = 1;
  for (ll S = 1; S < (1ll << n); S++) {
    int i = S & -S, nS = S ^ i;
    dis[S] = dis[nS] + dis[i];
    if(dis[S] == b[0] || dis[S] == b[1]) continue ;
    solve(S, i);
  }
  cout << f[(1<<n)-1] << '\n';
  return 0;
}

1.2 换根dp

P2986 [USACO10MAR] Great Cow Gathering G

\(siz_i\) 表示以 \(i\) 节点为子树的贡献之和(单个节点节点贡献为 \(C_i\))。

然后设 \(f_i\) 表示以 \(i\) 为根结点的贡献,并求出 \(f_1\)

而转移仅对父节点与子节点有影响,考虑换根。

\(x\) 的子结点 \(y\) 换为根时,\(y\) 子树内的贡献有消除,而 \(x\) 的所有子树除 \(y\) 子树外的贡献全部增加。

则有转移 \(f_y=f_x - siz_y \times w + (Sum - siz_y) \times w\),其中 \(x\)\(y\) 的父节点,\(Sum\)\(\sum siz_i\)\(w\)\(x\)\(y\) 之间的边权。

取最小值即可,时间复杂度 \(O(n)\)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 0x3f3f3f3f

using namespace std;

const int N = 1e5 + 10;

struct Node {
  int v, w, nx;
} e[N << 1];

int n, a[N], h[N], tot, d[N], f[N], siz[N], ans = inf, Sum;

void add(int u, int v, int w) {
  e[++tot] = (Node){v, w, h[u]};
  h[u] = tot;
}

void dfs(int x, int fa) {
  siz[x] = a[x];
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v, w = e[i].w;
    if(y == fa) continue;
    dfs(y, x);
    siz[x] += siz[y];
    d[x] += d[y] + siz[y] * w;
  }
}

void dfs1(int x, int fa) {
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v, w = e[i].w;
    if(y == fa) continue;
    f[y] = 1ll * f[x] - siz[y] * w + (Sum - siz[y]) * w;
    ans = min(ans, f[y]);
    dfs1(y, x);
  }
}

signed main() {
  cin >> n;
  For(i,1,n) cin >> a[i], Sum += a[i];
  For(i,1,n-1) {
    int u, v, w;
    cin >> u >> v >> w;
    add(u, v, w);
    add(v, u, w);
  }
  dfs(1, 0);
  f[1] = d[1];
  // ans = d[1];
  dfs1(1, 0);
  cout << ans << '\n';
  return 0;
}

2. 数据结构

2.1 分块

LOJ6277. 数列分块入门 1

\(p_i\) 表示位置 \(i\) 所在块的编号,\(L\) 为块长,最好是将 \(p_i = \frac{(i-1)}{L}+1\)。这样可以保证除尾部块的所有块连续完整。

然后散块暴力加,整块打标记。查答案时直接将标记和与暴力加过的 \(a_i\) 相加即为答案。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i) 
using namespace std;

const int N = 5e4 + 10;

int n, L, p[N], a[N], add[N];

void upd(int l, int r, int k) {
  if(p[l] == p[r]) {
    For(i,l,r) a[i] += k;
    return ;
  }
  For(i,p[l]+1,p[r]-1) add[i] += k;
  for (int i = l; p[l] == p[i]; ++i) a[i] += k;
  for (int i = r; p[r] == p[i]; --i) a[i] += k;
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  L = sqrt(n);
  For(i,1,n) cin >> a[i];
  For(i,1,n) p[i] = (i-1) / L + 1;
  For(i,1,n) {
    int op, l, r, c;
    cin >> op >> l >> r >> c;
    if(op == 0) {
      upd(l, r, c);
    } else {
      cout << a[r] + add[p[r]] << '\n';
    }
  }
  return 0;
}

LOJ6278. 数列分块入门 2

区间小于 \(k\) 的个数,首先想到二分。

然后散块暴力,整块二分求值即可。

但是二分先要排序,由于区间加,可能一个散块内修改后的顺序会被打乱。这时只需要直接重构即可。

先预处理块内的排序,修改到散块再重构,最后暴力+二分统计答案。

应常数吸吸O2才可过。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i) 
using namespace std;

const int N = 5e4 + 10, M = 250;

int n, a[N], L, p[N], add[N], Maxp, LL[N], RR[N];

vector<int> b[M];

void reset(int k) {
  vector<int>().swap(b[k]);
  For(i,LL[k],RR[k]) b[k].push_back(a[i]);
  sort(b[k].begin(), b[k].end());
  return ;
}

void upd(int l, int r, int k) {
  if(p[l] == p[r]) {
    For(i,l,r) a[i] += k;
    reset(p[l]);
    return ;
  }
  For(i,p[l]+1,p[r]-1) add[i] += k;
  for (int i = l; p[i] == p[l]; ++i) a[i] += k;
  reset(p[l]);
  for (int i = r; p[i] == p[r]; --i) a[i] += k;
  reset(p[r]);
  return ;
}

int ask(int l, int r, int k) {
  int ans = 0; k *= k;
  if(p[l] == p[r]) {
    For(i,l,r) ans += (a[i] + add[p[l]] < k);
    return ans;
  }
  For(i,p[l]+1,p[r]-1) {
    ans += lower_bound(b[i].begin(), b[i].end(), k - add[i]) - b[i].begin();
  }
  for (int i = l; p[i] == p[l]; ++i) ans += (a[i] + add[p[i]] < k);
  for (int i = r; p[i] == p[r]; --i) ans += (a[i] + add[p[i]] < k);
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  L = sqrt(n);
  For(i,1,n) cin >> a[i];
  For(i,1,n) {
    p[i] = (i-1)/L+1; Maxp = p[i];
    b[p[i]].push_back(a[i]);
  }
  For(i,1,Maxp) {
    LL[i] = RR[i-1] + 1;
    RR[i] = min(n, LL[i] + L - 1);
    sort(b[i].begin(), b[i].end());
  }
  For(i,1,n) {
    int op, l, r, c;
    cin >> op >> l >> r >> c;
    if(op == 0) {
      upd(l, r, c);
    } else {
      cout << ask(l, r, c) << '\n';
    }
  }
  return 0;
}

LOJ6279. 数列分块入门 3

思路与数列分块入门 2差不太多,都是排序之后散块暴力统计,整块二分统计。

同样遇到修改散块就重构一下。

注意:由于是前驱,所以位置为 lower_bound 的位置减一。若搜到了第一个位置,则没有前驱,直接跳过即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;

const int N = 1e5 + 10, M = 320;

int n, L, a[N], p[N], add[N], LL[N], RR[N], Maxp;

vector<int> v[N];

void reset(int k) {
  vector<int>().swap(v[k]);
  For(i,LL[k],RR[k]) v[k].push_back(a[i]);
  sort(v[k].begin(), v[k].end());
  return ;
}

void upd(int l, int r, int k) {
  if(p[l] == p[r]) {
    For(i,l,r) a[i] += k;
    reset(p[l]);
    return ;
  }
  For(i,p[l]+1,p[r]-1) add[i] += k;
  for (int i = l; p[l] == p[i]; ++i) a[i] += k;
  reset(p[l]);
  for (int i = r; p[r] == p[i]; --i) a[i] += k;
  reset(p[r]);
  return ;
}

int ask(int l, int r, int k) {
  int ans = INT_MIN;
  if(p[l] == p[r]) {
    For(i,l,r) if(a[i] + add[p[l]] < k) ans = max(ans, a[i] + add[p[l]]);
    return (ans == INT_MIN ? -1 : ans);
  }
  For(i,p[l]+1,p[r]-1) {
    auto it = lower_bound(v[i].begin(), v[i].end(), k - add[i]);
    if(it == v[i].begin()) continue ;
    it--;
    ans = max(ans, *(it) + add[i]);
  }
  for (int i = l; p[l] == p[i]; ++i) if(a[i] + add[p[l]] < k) ans = max(ans, a[i] + add[p[l]]);
  for (int i = r; p[r] == p[i]; --i) if(a[i] + add[p[r]] < k) ans = max(ans, a[i] + add[p[r]]);
  return (ans == INT_MIN ? -1 : ans);
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  L = sqrt(n);
  For(i,1,n) cin >> a[i];
  For(i,1,n) {
    p[i] = (i-1)/L+1;
    Maxp = p[i];
    v[p[i]].push_back(a[i]);
  }
  For(i,1,Maxp) {
    LL[i] = RR[i-1] + 1;
    RR[i] = min(n, LL[i] + L - 1);
    sort(v[i].begin(), v[i].end());
  }
  For(i,1,n) {
    int op, l, r, c;
    cin >> op >> l >> r >> c;
    if(op == 0) {
      upd(l, r, c);
    } else {
      cout << ask(l, r, c) << '\n';
    }
  }
  return 0;
}

LOJ6280. 数列分块入门 4

记一个 \(add\) 数组维护块内加,\(sum\) 数组维护块和,散快暴力加不标记,整块 \(sum\) 加并标记。

最后查询时散块直接加并加上标记,整块加 \(sum\) 即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;

const int N = 5e4 + 10;

int n, L, p[N], sum[N], a[N], add[N]; 

void upd(int l, int r, int k) {
  if(p[l] == p[r]) {
    For(i,l,r) {
      a[i] += k;
      sum[p[l]] += k;
    }
    return ;
  }
  For(i,p[l]+1,p[r]-1) add[i] += k, sum[i] += k * L;
  for (int i = l; p[i] == p[l]; i++) {
    a[i] += k;
    sum[p[l]] += k;
  }
  for (int i = r; p[i] == p[r]; i--) {
    a[i] += k;
    sum[p[r]] += k;
  }
  return ;
}

int ask(int l, int r, int c) {
  c++; int ans = 0;
  if(p[l] == p[r]) {
    For(i,l,r) ans = (ans + a[i] % c + add[p[l]] % c) % c;
    return ans % c;
  }
  For(i,p[l]+1,p[r]-1) ans = (ans + sum[i] % c) % c;
  for (int i = l; p[i] == p[l]; i++) ans = (ans + a[i] % c + add[p[l]] % c) % c;
  for (int i = r; p[i] == p[r]; i--) ans = (ans + a[i] % c + add[p[r]] % c) % c;
  return ans % c;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  L = sqrt(n);
  For(i,1,n) cin >> a[i], p[i] = (i-1)/L+1;
  For(i,1,n) sum[p[i]] += a[i];
  For(i,1,n) {
    int op, l, r, c;
    cin >> op >> l >> r >> c;
    if(op == 0) {
      upd(l, r, c);
    } else {
      cout << ask(l, r, c) << '\n';
    }
  }
  return 0;
}

LOJ6281. 数列分块入门 5

考虑到对小于等于 \(1\) 的整数开根还是等于它本身,而 \(10^9\)\(5\) 次以内的根就会小于等于 \(1\)

于是对于散块暴力修改,整块若有大于 \(1\) 的整数便全部开根。与此同时维护 \(sum\) 表示块内和与 \(b\) bool数组表示块内是否需要开根。

然后每次整块开完根后检查是否全部小于等于 \(1\) 即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;

const int N = 5e4 + 10;

int n, a[N], sum[N], b[N], p[N], L, LL[N], RR[N], Maxp;

void reset(int k) {
  sum[k] = 0;
  bool f = 1;
  For(i,LL[k],RR[k]) {
    a[i] = sqrt(a[i]);
    f &= (a[i] <= 1);
    sum[k] += a[i];
  }
  b[k] = f;
  return ;
}

void upd(int l, int r) {
  if(p[l] == p[r]) {
    For(i,l,r) {
      sum[p[l]] -= a[i];
      a[i] = sqrt(a[i]);
      sum[p[l]] += a[i];
    }
    return ;
  }
  For(i,p[l]+1,p[r]-1) {
    if(!b[i]) reset(i); 
  }
  for (int i = l; p[i] == p[l]; ++i) {
    sum[p[l]] -= a[i];
    a[i] = sqrt(a[i]);
    sum[p[l]] += a[i];
  }
  for (int i = r; p[i] == p[r]; --i) {
    sum[p[r]] -= a[i];
    a[i] = sqrt(a[i]);
    sum[p[r]] += a[i];
  }
  return ;
}

int ask(int l, int r) {
  int ans = 0;
  if(p[l] == p[r]) {
    For(i,l,r) ans += a[i];
    return ans;
  }
  For(i,p[l]+1,p[r]-1) ans += sum[i];
  for (int i = l; p[i] == p[l]; ++i) ans += a[i];
  for (int i = r; p[i] == p[r]; --i) ans += a[i];
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  L = sqrt(n);
  For(i,1,n) cin >> a[i];
  For(i,1,n) p[i] = (i-1)/L+1, Maxp = p[i], sum[p[i]] += a[i];
  For(i,1,Maxp) {
    LL[i] = RR[i-1] + 1;
    RR[i] = min(n, LL[i] + L - 1);
  }
  For(i,1,n) {
    int op, l, r, c;
    cin >> op >> l >> r >> c;
    if(op == 0) {
      upd(l, r);
    } else {
      cout << ask(l, r) << '\n';
    }
  }
  return 0;
}

LOJ6283. 数列分块入门 7

与线段数2小像...

维护 \(mul\) 块内乘法和 \(sum\) 块内加法,然后整块操作1,2尽管打标记即可。

散块直接暴力可能会遇到一些问题,就是块内的元素标记还没传下来算完,就加/乘了。答案显然不正确。于是散块先要暴力下传标记,后暴力加/乘即可。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 10007

using namespace std;

const int N = 1e5 + 10;

int n, a[N], p[N], mul[N], add[N], LL[N], RR[N], L, Maxp;

void reset(int k) {
  For(i,LL[k],RR[k]) {
    a[i] *= mul[k]; a[i] %= mod;
    a[i] += add[k]; a[i] %= mod;
  }
  mul[k] = 1, add[k] = 0;
  return ;
}

void updd(int l, int r, int c) {
  c %= mod;
  if(p[l] == p[r]) {
    reset(p[l]);//
    For(i,l,r) a[i] = (a[i] + c) % mod;
    return ;
  }
  For(i,p[l]+1,p[r]-1) add[i] = (add[i] + c) % mod;
  reset(p[l]);//
  for (int i = l; p[i] == p[l]; ++i) a[i] = (a[i] + c) % mod;
  reset(p[r]);//
  for (int i = r; p[i] == p[r]; --i) a[i] = (a[i] + c) % mod;
  return ;
}

void updm(int l, int r, int c) {
  c %= mod;
  if(p[l] == p[r]) {
    reset(p[l]);//
    For(i,l,r) a[i] = (a[i] * c) % mod;
    return ;
  }
  For(i,p[l]+1,p[r]-1) add[i] = (add[i] * c) % mod, mul[i] = (mul[i] * c) % mod;
  reset(p[l]);//
  for (int i = l; p[i] == p[l]; ++i) a[i] = (a[i] * c) % mod;
  reset(p[r]);//
  for (int i = r; p[i] == p[r]; --i) a[i] = (a[i] * c) % mod;
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  L = sqrt(n);
  For(i,1,n) cin >> a[i], p[i] = (i-1)/L+1, Maxp = p[i];
  For(i,1,Maxp) {
    LL[i] = RR[i-1] + 1;
    RR[i] = min(n, LL[i] + L - 1);
    mul[i] = 1; add[i] = 0;
  }
  For(i,1,n) {
    int op, l, r, c;
    cin >> op >> l >> r >> c;
    if(op == 0) {
      updd(l, r, c);
    } else if(op == 1) {
      updm(l, r, c);
    } else {
      reset(p[r]);
      cout << a[r] << '\n';
    }
  }
  return 0;
}

/*
S * mul + add
*/

luoguP5356 [Ynoi2017] 由乃打扑克

发现第 \(k\) 小很难维护,直接的做法便是全部拉出来线段树二分。

发现瓶颈在我们不知道第 \(k\) 小的数为多少,也很难优秀的求出。那就枚举第 \(k\) 小的数 \(x\),这样就相当于已知 \(x\),求小于 \(x\) 的个数是否为 \(k\)。这个可以利用类似于 LOJ6278. 数列分块入门 2 的思想进行分块求解。

而枚举又太劣,发现这玩意有单调性,直接二分即可。

但是写完发现过不了被卡,因为这是由乃OI

二分值域优化:每一次修改操作最多让值域扩大 \(2\times 10^4\),二分时的值域跟着最值更新。

块内最值优化:如果块内的最小值都比 \(x\) 大,说明没有数小于 \(x\),直接跳过。如果块内的最大值都比 \(x\) 小,说明块内所有数小于 \(x\),加上块长跳过。

块长优化:调整块长为 \(65\)\(90\)

于是愉快的 AC 了!

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 2000000000
using namespace std;

const int N = 1e5 + 10, M = 505;

int n, m, a[N], L, LL[N], RR[N], p[N], add[N], Maxp, Max[N], Min[N], cnt = 1;

vector<int> b[N];

void reset(int k) {
  vector<int>().swap(b[k]);
  For(i,LL[k],RR[k]) b[k].push_back(a[i]);
  sort(b[k].begin(), b[k].end());
  Min[k] = b[k][0], Max[k] = b[k][b[k].size()-1];
  return ;
}

void upd(int l, int r, int k) {
  if(p[l] == p[r]) {
    For(i,l,r) a[i] += k;
    reset(p[l]);
    return ;
  }
  For(i,p[l]+1,p[r]-1) add[i] += k;
  for (int i = l; p[i] == p[l]; ++i) a[i] += k;
  reset(p[l]);
  for (int i = r; p[i] == p[r]; --i) a[i] += k;
  reset(p[r]);
  return ;
}

int ask(int l, int r, int k) {
  int ans = 0;
  if(p[l] == p[r]) {
    For(i,l,r) ans += (a[i] + add[p[l]] < k);
    return ans;
  }
  For(i,p[l]+1,p[r]-1) {
    if(Max[i] + add[i] < k) {
      ans += L; continue;
    }
    if(Min[i] + add[i] > k) {
      continue;
    }
    ans += lower_bound(b[i].begin(), b[i].end(), k - add[i]) - b[i].begin();
  }
  for (int i = l; p[i] == p[l]; ++i) ans += (a[i] + add[p[i]] < k);
  for (int i = r; p[i] == p[r]; --i) ans += (a[i] + add[p[i]] < k);
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  L = 90;
  For(i,1,n) cin >> a[i];
  For(i,1,n) {
    p[i] = (i-1)/L+1; Maxp = p[i];
    b[p[i]].push_back(a[i]);
  }
  For(i,1,Maxp) {
    LL[i] = RR[i-1] + 1;
    RR[i] = min(n, LL[i] + L - 1);
    sort(b[i].begin(), b[i].end());
    Min[i] = b[i][0], Max[i] = b[i][b[i].size()-1];
  }
  while(m--) {
    int op, l, r, k;
    cin >> op >> l >> r >> k;
    if(op == 1) {
      if(r - l + 1 < k) {
        puts("-1"); continue;
      }
      int x = -2e4 * cnt - 5000, y = -x;
      while(x <= y) {
        int mid = x + y >> 1;
        if(ask(l, r, mid) < k) x = mid + 1;
        else y = mid - 1;
      }
      cout << x - 1 << '\n';
    } else {
      upd(l, r, k);
      cnt++;
    }
  }
  return 0;
}

luoguP2801 教主的魔法

其实就是 LOJ6278. 数列分块入门 2 中的 $< k $ 换成 \(\geq k\)

然后再加上块内最值优化与块长优化(块长调为 \(850\)),开O2可以跑600ms以内。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i) 
using namespace std;

const int N = 1e6 + 5, M = 1e3 + 5;

int n, q, a[N], L, p[N], add[N], Maxp, Max[N], Min[N], LL[N], RR[N];

vector<int> b[N];

void reset(int k) {
  vector<int>().swap(b[k]);
  For(i,LL[k],RR[k]) b[k].push_back(a[i]);
  sort(b[k].begin(), b[k].end());
  Max[k] = b[k][b[k].size()-1], Min[k] = b[k][0];
  return ;
}

void upd(int l, int r, int k) {
  if(p[l] == p[r]) {
    For(i,l,r) a[i] += k;
    reset(p[l]);
    return ;
  }
  For(i,p[l]+1,p[r]-1) add[i] += k;
  for (int i = l; p[i] == p[l]; ++i) a[i] += k;
  reset(p[l]);
  for (int i = r; p[i] == p[r]; --i) a[i] += k;
  reset(p[r]);
  return ;
}

int ask(int l, int r, int k) {
  int ans = 0;
  if(p[l] == p[r]) {
    For(i,l,r) ans += (a[i] + add[p[l]] >= k);
    return ans;
  }
  For(i,p[l]+1,p[r]-1) {
    if(Min[i] + add[i] >= k) {
      ans += L; continue;
    }
    if(Max[i] + add[i] < k) continue;
    int x = lower_bound(b[i].begin(), b[i].end(), k - add[i]) - b[i].begin();
    ans += L - x;
  }
  for (int i = l; p[i] == p[l]; ++i) ans += (a[i] + add[p[i]] >= k);
  for (int i = r; p[i] == p[r]; --i) ans += (a[i] + add[p[i]] >= k);
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> q;
  L = 850;
  For(i,1,n) cin >> a[i];
  For(i,1,n) {
    p[i] = (i-1)/L+1; Maxp = p[i];
    b[p[i]].push_back(a[i]);
  }
  For(i,1,Maxp) {
    LL[i] = RR[i-1] + 1;
    RR[i] = min(n, LL[i] + L - 1);
    sort(b[i].begin(), b[i].end());
    Max[i] = b[i][b[i].size()-1], Min[i] = b[i][0];
  }
  while(q--) {
    int l, r, c;
    char op;
    cin >> op >> l >> r >> c;
    if(op == 'M') {
      upd(l, r, c);
    } else {
      cout << ask(l, r, c) << '\n';
    }
  }
  return 0;
}

2.2 平衡树

luoguP3369【模板】普通平衡树

平衡树模板,现在只会 treap(有旋的忘了)

可以参考yzy的学习笔记【算法】平衡树

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007

using namespace std;

inline int read() {
  rint x=0,f=1;char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
  return x*f;
}

void print(int x){
  if(x<0){putchar('-');x=-x;}
  if(x>9){print(x/10);putchar(x%10+'0');}
  else putchar(x+'0');
  return;
}

const int N = 1e5 + 10;

struct Node {
  int l, r, val, key, siz;
} t[N];

int n, cnt, root;

mt19937 rd(114514);

int Mknode(int val) {//建立新节点
  t[++cnt].val = val;
  t[cnt].key = rd();
  t[cnt].siz = 1;
  return cnt;
}

void pushup(int p) {//上传信息
  t[p].siz = t[t[p].l].siz + t[t[p].r].siz + 1; 
  return ;
}

void split(int p, int val, int &x, int &y) {
  if(!p) {
    x = y = 0;
    return ;
  }
  if(t[p].val <= val) {
    x = p;
    split(t[p].r, val, t[p].r, y);
  } else {
    y = p;
    split(t[p].l, val, x, t[p].l);
  }
  pushup(p);
}

int merge(int x, int y) {
  if(!x || !y) return x + y;
  if(t[x].key > t[y].key) {
    t[x].r = merge(t[x].r, y);
    pushup(x);
    return x;
  } else {
    t[y].l = merge(x, t[y].l);
    pushup(y);
    return y;
  }
}

int x, y, z;

void Ins(int val) {
  split(root, val, x, y);
  root = merge(merge(x, Mknode(val)), y);
}

void Del(int val) {
  split(root, val, x, z);
  split(x, val-1, x, y);
  y = merge(t[y].l, t[y].r);
  root = merge(merge(x, y), z)  ;
}

int rk(int val) {
  split(root, val-1, x, y);
  int ans = t[x].siz + 1;
  root = merge(x, y);
  return ans;
}

int kth(int x) {
  int p = root;
  while(p) {
    if(t[t[p].l].siz + 1 == x) return t[p].val;
    else if(t[t[p].l].siz >= x) p = t[p].l;
    else x -= (t[t[p].l].siz + 1), p = t[p].r; 
  }
}

int pre(int val) {
  split(root, val-1, x, y);
  int p = x;
  while(t[p].r) p = t[p].r;
  int ans = t[p].val;
  root = merge(x, y);
  return ans;
}

int nxt(int val) {
  split(root, val, x, y);
  int p = y;
  while(t[p].l) p = t[p].l;
  int ans = t[p].val;
  root = merge(x, y);
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  while(n--) {
    int op, x; cin >> op >> x;
    if(op == 1) Ins(x);
    else if(op == 2) Del(x);
    else if(op == 3) cout << rk(x) << '\n';
    else if(op == 4) cout << kth(x) << '\n';
    else if(op == 5) cout << kth(rk(x) - 1) << '\n';
    else cout << kth(rk(x + 1)) << '\n';
  }
  return 0;
}

CF799B T-shirt buying

考虑到 \(a_i,b_i\in[1,3]\),直接开三个 set 然后放入每个颜色对应的物品价格即可。

由于一个物品有两种颜色,卖完后删去其中一个 set 的物品价格,但是还有一个颜色的对应的物品价格没有删去。这时候只要将物品的编号也插入 set 中,然后延迟删除即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define val first
#define id second
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 1000000010

using namespace std;

const int N = 2e5 + 10;

int n, m, p[N], a[N], b[N]; 

multiset<pair<int, int> > s[4];

map<int, bool> mp;

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  For(i,1,n) cin >> p[i];
  For(i,1,n) cin >> a[i];
  For(i,1,n) cin >> b[i];
  For(i,1,n) {
    s[a[i]].insert({p[i], i});
    s[b[i]].insert({p[i], i});
  }
  cin >> m;
  while(m--) {
    int x; cin >> x;
    if(!s[x].size()) cout << "-1 ";
    else {
      pair<int, int> P = *s[x].begin();
      while(mp[P.id]) {
        s[x].erase(s[x].begin());
        if(!s[x].size()) {
          cout << "-1 ";
          goto yzy;
        }
        P = *s[x].begin();
      }
      cout << P.first << ' ';
      mp[P.id] = 1;
      s[x].erase(s[x].begin());
      yzy: ;
    }
  }
  return 0;
} 

2.3 KD-tree

luoguP4148 简单题

KD-tree 模板,参考yzy的学习笔记 【算法】KD-tree

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;

const int N = 2e5 + 10, LG = 21;

struct KD_tree {
  int x[2], v;
  int l, r, sum;
  int L[2], R[2];
} t[N], lf, rh;

int n, rt[30], b[N], cnt;

void pushup(int p) {
  t[p].sum = t[t[p].l].sum + t[t[p].r].sum + t[p].v;
  for (int k : {0, 1}) {
    t[p].R[k] = t[p].L[k] = t[p].x[k];
    if(t[p].l) {
      t[p].L[k] = min(t[p].L[k], t[t[p].l].L[k]);
      t[p].R[k] = max(t[p].R[k], t[t[p].l].R[k]);
    }
    if(t[p].r) {
      t[p].L[k] = min(t[p].L[k], t[t[p].r].L[k]);
      t[p].R[k] = max(t[p].R[k], t[t[p].r].R[k]);
    }
  }
}

int build(int l, int r, int D = 0) {
  int mid = l + r >> 1;
  nth_element(b + l, b + mid, b + r + 1, [D](int x, int y){return t[x].x[D] < t[y].x[D];});
  int x = b[mid];
  if(l < mid) t[x].l = build(l, mid - 1, D ^ 1);
  if(r > mid) t[x].r = build(mid + 1, r, D ^ 1);
  pushup(x);
  return x;
}

int query(int p) {
  if(!p) return 0;
  bool f = 1; int ans = 0;
  for (int k : {0, 1}) {
    f &= (t[p].L[k] >= lf.x[k] && t[p].R[k] <= rh.x[k]);
  }
  if(f) return t[p].sum;
  for (int k : {0, 1}) {
    if(t[p].R[k] < lf.x[k] || t[p].L[k] > rh.x[k]) return 0;
  } 
  f = 1;
  for (int k : {0, 1}) {
    f &= (lf.x[k] <= t[p].x[k] && t[p].x[k] <= rh.x[k]);
  }
  if(f) ans = t[p].v;
  return ans + query(t[p].l) + query(t[p].r);
}

void Add(int &p) {
  if(!p) return ;
  b[++cnt] = p;
  Add(t[p].l);
  Add(t[p].r);
  p = 0;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  n = 0; int lst = 0;
  while(1) {
    int op; cin >> op;
    if(op == 3) break;
    else if(op == 1) {
      int x, y, w; cin >> x >> y >> w;
      x ^= lst, y ^= lst, w ^= lst;
      t[++n] = {{x, y}, w};
      b[cnt=1] = n;
      for (int siz = 0; ; siz++) {
        if(!rt[siz]) {
          rt[siz] = build(1, cnt);
          break;
        } else {
          Add(rt[siz]);
        }
      }
    } else {
      cin >> lf.x[0] >> lf.x[1] >> rh.x[0] >> rh.x[1];
      lf.x[0] ^= lst;
      lf.x[1] ^= lst;
      rh.x[0] ^= lst;
      rh.x[1] ^= lst;
      lst = 0;
      For(i,0,LG) lst += query(rt[i]);
      cout << lst << '\n';
    }
  }
  return 0;
}

/*
4
1 2 3 3
2 1 1 3 3
1 1 1 1
2 1 1 0 7
3
*/

2.4 线段树

P2633 Count on a tree

区间的第 \(k\) 大可以想到主席树,而树上的路径第 \(k\) 大可以将主席树搬到树上维护。

考虑主席树类似于前缀和做维护,每一次可持久化父结点的线段树信息,并记录该结点本身的信息。

询问时只要求出 LCA,然后根据公式 \(val_u+val_v-val_{LCA},-val_{fa[LCA]}\) 在主席树上二分即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 2147483949

using namespace std;

const int N = 5e5 + 10, M = 1e7 + 10;

struct Node {
  int v, nx;
} e[N << 1];

struct Seg {
  int ls, rs, val;
} t[M];

int n, m, h[N], dep[N], anc[N][21], a[N], tot, root[N], fa[N], idx, lst;

void add(int u, int v) {
  e[++tot] = (Node){v, h[u]};
  h[u] = tot;
}

void pushup(int p) {
  t[p].val = t[t[p].ls].val + t[t[p].rs].val;
}

void upd(int la, int &p, int L, int R, int x) {
  p = ++idx;
  t[p] = t[la];
  if(L == R) {
    t[p].val++;
    return ;
  }
  int mid = L + R >> 1;
  if(x <= mid) upd(t[la].ls, t[p].ls, L, mid, x);
  else upd(t[la].rs, t[p].rs, mid + 1, R, x);
  pushup(p);
}

int ask(int x, int y, int z, int l, int L, int R, int k) {
  if(L == R) return L;
  int K = t[t[x].ls].val + t[t[y].ls].val - t[t[z].ls].val - t[t[l].ls].val;
  int mid = L + R >> 1;
  if(k <= K) return ask(t[x].ls, t[y].ls, t[z].ls, t[l].ls, L, mid, k);
  else return ask(t[x].rs, t[y].rs, t[z].rs, t[l].rs, mid + 1, R, k - K);
}

void dfs(int x, int f) {
  dep[x] = dep[f] + 1;
  fa[x] = f;
  upd(root[f], root[x], 1, inf, a[x]);
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(y == f) continue;
    anc[y][0] = x;
    dfs(y, x);
  }
}

void init() {
  For(j,1,19) {
    For(i,1,n) {
      anc[i][j] = anc[anc[i][j-1]][j-1];
    }
  }
}

int lca(int x, int y) {
  if(dep[x] < dep[y]) swap(x, y);
  FOR(i,19,0) if(dep[anc[x][i]] >= dep[y]) x = anc[x][i];
  if(x == y) return x;
  FOR(i,19,0) if(anc[x][i] != anc[y][i]) x = anc[x][i], y = anc[y][i];
  return anc[x][0];
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  For(i,1,n) cin >> a[i];
  For(i,1,n-1) {
    int u, v;
    cin >> u >> v;
    add(u, v); add(v, u);
  }
  dfs(1, 0);
  init();
  while(m--) {
    int u, v, k;
    cin >> u >> v >> k;
    u ^= lst;
    int LCA = lca(u, v);
    lst = ask(root[u], root[v], root[LCA], root[fa[LCA]], 1, inf, k);
    cout << lst << '\n';
  }
  return 0;
}

3. 图论专题

3.1 网络流

luogu1402 酒店之王

考虑按怎样的顺序建模。

如果按照“人, 房间, 菜”的顺序建模,就有可能出现一个人选择多个房间的情况

显然可以发现把人这一类放在中间进行建模是最好的,按照“房间,人 , 菜”的顺序,依据人的喜好连边。

但是注意每一个人只能选择一个房间和一道菜。所以要将人结点拆开分成两个点。中间连一条 \(1\) 容量的边。

最后跑最大流即为答案。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 10000000000

using namespace std;

const int N = 1e5 + 10;

struct Node {
  int v, nx, w;
} e[N << 1];

int n, p, q, h[N], tot = 1, s, t, dep[N], cur[N];

void add(int u, int v, int w) {
  e[++tot] = (Node){v, h[u], w};
  h[u] = tot;
}

bool bfs() {
  For(i,1,n) dep[i] = 0;
  queue<int> q;
  q.push(s);
  dep[s] = 1;
  while(!q.empty()) {
    int x = q.front();
    q.pop();
    for (int i = h[x]; i; i = e[i].nx) {
      int y = e[i].v;
      if(e[i].w > 0 && !dep[y]) {
        dep[y] = dep[x] + 1;
        q.push(y); 
      }
    }
  }
  return dep[t] != 0;
}

int dfs(int x, int flow) {
  int res = 0;
  if(x == t) return flow;
  for (int &i = cur[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(e[i].w > 0 && dep[y] == dep[x] + 1) {
      int k = dfs(y, min(flow - res, e[i].w));
      res += k, e[i].w -= k, e[i ^ 1].w += k;
      if(res == flow) return res;
    }
  }
  return res;
}

int Dinic() {
  int ans = 0;
  while(bfs()) {
    memcpy(cur, h, sizeof cur);
    ans += dfs(s, inf);
  }
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> p >> q;
  For(i,1,n) {
    add(i, i + n, 1);
    add(i + n, i, 0);
  } 
  int id = 2 * n;
  For(i,1,n) {
    For(j,1,p) {
      int op; cin >> op;
      if(op) {
        add(id + j, i, 1);
        add(i, id + j, 0);
      } 
    }
  }
  id += p;
  For(i,1,n) {
    For(j,1,q) {
      int op; cin >> op;
      if(op) {
        add(i + n, id + j, 1);
        add(id + j, i + n, 0);
      } 
    }
  }
  id += q;
  s = id + 1, t = id + 2;
  For(i, 2 * n + 1, 2 * n + p) {
    add(s, i, 1); add(i, s, 0);
  }
  For(i, 2 * n + p + 1, 2 * n + p + q) {
    add(i, t, 1); add(t, i, 0);
  }
  n = id + 2;
  cout << Dinic() << '\n';
  return 0;  
}

注意一下结点的编号即可。双倍经验

posted @ 2024-06-29 16:48  Daniel_yzy  阅读(23)  评论(0编辑  收藏  举报