【做题笔记】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;
}
注意一下结点的编号即可。双倍经验