模板索引:数据结构
单调栈
例题 B3666
给定一个数列\(a\),初始为空。有\(n\)次操作,每次在\(a\)的末尾添加一个正整数\(x\)
每次操作结束后,请你找到当前\(a\)所有的后缀最大值的下标(下标从 1 开始)。一个下标\(i\)是当前\(a\)的后缀最大值下标当且仅当:对于所有的>\(i<j\leq|a|\),都有\(a_i>a_j\),其中\(|a|\)表示当前\(a\)的元素个数。
为了避免输出过大,请你每次操作结束后都输出一个整数表示当前数列所有后缀最大值的下标的按位异或和。
\(1\leq n\leq10^6,1\leq x_i<2^{64}.\)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
int n;
ull ans;
vector<ull> vc;
vector<ull> tmp; // 这个不是栈,这个存的是原数组
int main(){
cin >> n;
tmp.push_back(1);
for(int i=1; i<=n; i++){
ull x = read();
tmp.push_back(x);
while(!vc.empty() && x >= tmp[vc.back()]){
ans ^= vc.back();
vc.pop_back();
}
ans ^= i;
vc.push_back(i);
printf("%d\n", ans);
}
return 0;
}
P6503 [COCI 2010/2011 #3] DIFERENCIJA
给出一个长度为 \(n\) 的序列 \(a_i\),求出下列式子的值:
\[\sum_{i=1}^{n} \sum_{j=i}^{n} (\max_{i\le k\le j} a_k-\min_{i\le k\le j} a_k) \]即定义一个子序列的权值为序列内最大值与最小值的差。求出所有连续子序列的权值和。
- 方法一
我们考虑对每个\(a_i\)算它的贡献。
注意到当且仅当\(a_i\)为区间\([l,r]\)的最大值时,\(a_i\)才会被计算一次。
那么现在只需要计算有多少个区间\([l,r]\)使得\(a_i\)为区间最大值。
设\(L\)为满足\(l<i,a_l\geqslant a_i\)的最大的\(l\) , \(R\)为满足\(r>i,a_r>a_i\)的最小的\(r\),那么这样的区间个数就是\((i-L)(R-i)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3e5+10;
int n;
int maxl[maxn], maxr[maxn], minl[maxn], minr[maxn];
ll ans, a[maxn];
vector<int> tmp;
int main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++){
while(!tmp.empty() && a[i] >= a[tmp.back()]){
maxr[tmp.back()] = i;
tmp.pop_back();
}
if(!tmp.size()) maxl[i] = 0;
else maxl[i] = tmp[tmp.size() - 1];
tmp.push_back(i);
}
while(!tmp.empty()){
maxr[tmp.back()] = n + 1;
tmp.pop_back();
}
for(int i = 1; i <= n; i++){
while(!tmp.empty() && a[i] <= a[tmp.back()]){
minr[tmp.back()] = i;
tmp.pop_back();
}
if(!tmp.size()) minl[i] = 0;
else minl[i] = tmp[tmp.size() - 1];
tmp.push_back(i);
}
while(!tmp.empty()){
minr[tmp.back()] = n + 1;
tmp.pop_back();
}
for(int i = 1; i <= n; i++){
ans += a[i]*(maxr[i] - i)*(i - maxl[i]);
ans -= a[i]*(minr[i] - i)*(i - minl[i]);
}
cout << ans << endl;
return 0;
}
- 方法二
我们设\(f_i/g_i\)为以\(i\)结尾的所有子序列的最大/最小值之和,那么答案为\(\sum f_i-g_i\)。
考虑如何维护\(f,g\)。
不妨设\(p_i<i\)为满足\(a_{p_i}>a_i\)的最大的\(p_i\) (特别的,如果\(p_i\)不存在则为0),那么有转移方程\(f_i=f_{p_i}+a_i\times(i-p_i),p_i\)可以用单调栈维护。
稍微解释一下上面的转移方程是如何得来的: - 因为\(a_i\)对\(p_i\)以及\(p_i\)以前的最大值没有影响 (即\([1,p_i]\)与\([1,i]\),\([2,p_i]\)与\([2,i]\cdots[p_i,p_i]\)与
\([p_i,i]\)的最大值相同),所以可以直接由\(f_{p_i}\)转移得来。 - 而根据\(p_i\)的定义,后面\(i-p_i\)个子序列 (即\([p_i+1,i],[p_i+2,i]\cdots,[i,i]\))的最大值为\(a_i\) ,
所以加上\(a_i\times(i-p_i)\)。 - 所以转移方程为\(f_i=f_{p_i}+a_i\times(i-p_i)\)。
点击查看代码
for(int i=1;i<=n;i++){
ll x=read(),p;
while(t1&&x>=a[t1].fi)t1--;
while(t2&&x<=b[t2].fi)t2--;
p=a[t1].se; f[i]=f[p]+x*(i-p);
p=b[t2].se; g[i]=g[p]+x*(i-p);
ans+=f[i]-g[i],a[++t1]=b[++t2]={x,i};
}
单调队列
例题 B3667
给定一个长度为\(n\)的数列\(a\),对于其中每个长度为\(k\)的子区间,请你求出这个这个子区间构成的数列的所有后缀最大值的位置个数。
一个下标\(i\)是是数列\(b\)的后缀最大值下标当且仅当:对于所有的\(i<j\leq|b|\),都有\(b_i>b_j\),其中\(|b|\)表示\(b\)的元素个数。
\(1\leq k\leq n\leq10^6,1\leq x_i<2^{64}.\)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
int n, k;
int main(){
cin >> n >> k;
vector<ull> a(n+1);
deque<int> q;
for(int i=1; i<=n; i++){
ull x = read();
int l = max(i-k+1, 1);
while(!q.empty() && q.front() < l ) q.pop_front();
while(!q.empty() && a[q.back()] <= x) q.pop_back();
q.push_back(i);
a[i] = x;
if(i-k+1 >= 1) printf("%d\n", q.size());
}
return 0;
}
带权并查集
P1196 银河英雄传说
题目链接
有一个划分成 n 列的星际战场,各列编号为 1~n。有 n 艘战舰,编号也为 1~n,初始时第 i号战舰处于第 i列。
有 m 条指令,每条指令为以下两种之一:
- M i j 表示让第 i号战舰所在列按原有顺序接在第 j 号战舰所在列后面。
- C i j 表示询问第 i,j号战舰之间间隔了多少艘战舰,或判断不在同一列。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3e4+10;
int f[maxn];
int n = 3e4, m, sz[maxn], d[maxn];
int find(int x){
if(x == f[x]) return x;
int rt = find(f[x]); // !
d[x] += d[f[x]]; // 注意这两条语句顺序。因为节点距root的距离是一层层累加下来的
return f[x] = rt;
}
void merge(int x, int y){
x = find(x); y = find(y);
f[x] = y; d[x] = sz[y]; // x距离root距离是被合并集合原本整个集合的大小
sz[y] += sz[x];
}
int ask(int x, int y){
int rtx = find(x), rty = find(y);
if(rtx != rty) return -1;
return abs(d[x] - d[y]) - 1;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> m; // 注意关闭同步流不要用read()了,很神秘的爆零了
// 千万要记得初始化!
for(int i = 1; i <= n; i++)
f[i] = i, sz[i] = 1;
for(int i = 1, x, y; i <= m; i++){
char ch;
cin >> ch >> x >> y;
if(ch == 'M') merge(x, y);
else cout << ask(x, y) << endl;
}
return 0;
}
拓展域并查集
P1525 关押罪犯
有 n 名罪犯要关押进两个监狱中。
这些罪犯相互之间有 m 个仇恨关系,每个仇恨关系为 x,y之间有怨气值为 z 的仇恨。如果 x,y不在同一个监狱,则仇恨关系作废。
你希望最小化最大的怨气值
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e4+10, maxm = 1e5+10;
int n, m, f[maxm*2];
struct node{
int x, y, z;
friend bool operator < (node cmpx, node cmpy){
return cmpx.z > cmpy.z;
}
}e[maxm];
int find(int x){
return x == f[x] ? x : f[x] = find(f[x]);
}
void merge(){}
int main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1, a, b, c; i <= m; i++)
cin >> e[i].x >> e[i].y >> e[i].z;
sort(e + 1, e + 1 + m);
iota(f + 1, f + 1 + n*2, 1);
for(int i = 1; i <= m; i++){
int x = find(e[i].x), y = find(e[i].y);
if(x == y) return cout<< e[i].z<< endl, 0;
int X = find(e[i].x + n), Y = find(e[i].y + n);
f[x] = Y; f[y] = X;
}cout << 0 << endl;
return 0;
}
拓展域并查集通过以下机制避免了贪心的问题:
虚拟点表示所有可能性:每个点的两种状态(\(A\) 或 \(B\)) 被显式建模为虚拟点。这相当于扩展了状态空间,允许并查集同时跟踪所有可能的选择,而不立即做出决策。
约束转化为等价关系:将“不能同集”约束转化为两个合并操作(\(\text{union}(i_A, j_B)\) 和 \(\text{union}(i_B, j_A)\)),这捕获了约束的完整逻辑:
\(\text{union}(i_A, j_B)\) 表示“如果 \(i\) 在 \(A\),则 \(j\) 必须在 \(B\)”。
\(\text{union}(i_B, j_A)\) 表示“如果 \(i\) 在 \(B\),则 \(j\) 必须在 \(A\)”。
这些合并操作建立了虚拟点之间的等价链,能够通过并查集的连通性传递关系。
所以并查集本质就是维护关系可能性,通过并查集的连通性,约束关系形成传递链。
- 普通并查集:维护"必须在一起"的等价关系,只处理单一可能性:i和j必须属于同一集合
- 拓展域并查集:维护"不能在一起"的互斥关系,同时处理两种可能性:
-
- 可能性1:i在A → j在B (对应i_A ≡ j_B)
-
- 可能性2:i在B → j在A (对应i_B ≡ j_A)
-
- 操作:同时执行union(i_A, j_B)和union(i_B, j_A)
例题P1892 [BalticOI 2003] 团伙
线段树
P3372 【模板】线段树 1
单点加、区间求和
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5e5+500;
ll w[maxn*4],a[maxn],lzy[maxn*4];
int n,m;
void pushup(int u){
w[u]=w[u*2]+w[u*2+1];
}
bool inrange(int l,int r,int L,int R){
return (L <= l) && (r <=R );
}
bool outofrange(int l,int r,int L,int R){
return (l > R) || (r < L);
}
void maketag(int u,int len,ll x){
lzy[u]+=x;
w[u]+=len*x;
}
void pushdown(int u,int l,int r){
if(lzy[u]){
int mid=(l+r)>>1;
maketag(u*2,mid-l+1,lzy[u]);
maketag(u*2+1,r-mid,lzy[u]);
lzy[u]=0;
}
}
void build(int u,int l,int r){
if(l==r){
w[u]=a[l];
return ;
}
int mid=(l+r)>>1;
build(u*2,l,mid);
build(u*2+1,mid+1,r);
pushup(u);
}
ll ddcx(int u,int l,int r,int p){
if(l==r){
return w[u];
}
int mid=(l+r)>>1;
pushdown(u,l,r);
if(p<=mid) return ddcx(u*2,l,mid,p);
else return ddcx(u*2+1,mid+1,r,p);
}
void ddxg(int u,int l,int r,int p,ll x){
if(l==r){
w[u]=x;
return ;
}
pushdown(u, L, R);
int mid=(l+r)>>1;
if(p<=mid) ddxg(u*2,l,mid,p,x);
else ddxg(u*2+1,mid+1,r,p,x);
pushup(u);
}
ll qjcx(int u,int l,int r,int L,int R){
if(inrange(l,r,L,R))
return w[u];
else if(outofrange(l,r,L,R))return 0;
else {
int mid=(l+r)>>1;
pushdown(u,l,r);
return qjcx(u*2,l,mid,L,R)+qjcx(u*2+1,mid+1,r,L,R);
}
}
void qjxg(int u,int l,int r,int L,int R,ll x){
if(inrange(l,r,L,R))maketag(u,r-l+1,x);
else if(!outofrange(l,r,L,R)){
int mid=(l+r)>>1;
pushdown(u,l,r);
qjxg(u*2,l,mid,L,R,x);
qjxg(u*2+1,mid+1,r,L,R,x);
pushup(u);
}
}
inline long long read(){
char readch=getchar(); ll readtmp=0;
ll readflag=1;
while(readch<'0' || '9'<readch) {if(readch=='-')readflag=-1;readch=getchar();}
while('0'<=readch && readch<='9') {readtmp=readtmp*10+readch-'0';readch=getchar();}
return readtmp*readflag;
}
int main(){
cin>>n>>m;
for(int i=1; i<=n; i++){
a[i]=read();
}
build(1,1,n);
while(m--){
int tmp;
tmp=read();
int x,y;
ll k;
if(tmp==1){
x=read();
y=read();
k=read();
qjxg(1,1,n,x,y,k);
}
else {
x=read();
y=read();
printf("%lld\n",qjcx(1,1,n,x,y));
}
}
return 0;
}
RMQ问题(ST表)
例 8: 喷泉(洛谷 P7167,By 一只书虫仔)
一个喷泉由 \(n\) 个圆盘组成,从上到下依次编号为 1 到 \(n\),第 \(i\) 个圆盘的直径为 \(d_i\),容量为 \(c_i\)。当一个圆盘里的水多于该圆盘的容量时,水会溢出往下流,直到流入半径大于该圆盘的圆盘里。如果下面没有满足要求的圆盘,水就会流到喷泉下面的水池里。
现在有 \(q\) 组询问,每一组询问描述:向第 \(r_i\) 个圆盘里倒入 \(v_i\) 的水,求水最后会流到哪一个圆盘停止。如果最终流入了水池里,那么输出 0。注意,每个询问互不影响,也就是每次向圆盘倒水前,所有的圆盘中都没有水。
数据范围:\(2 \leq n \leq 10^5\),\(1 \leq q \leq 2 \times 10^5\),\(1 \leq c_i \leq 1000\),\(1 \leq d_i, v_i \leq 10^9\),\(1 \leq r_i \leq n\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define inf 0x3f3f3f3f
const int maxn = 1e5+10;
int n, q, d[maxn], c[maxn], rmax[maxn][20], f[maxn][20], log_2[maxn], g[maxn][20];
int querymax(int l, int r){
int x = log_2[r-l+1];
return max(rmax[l][x], rmax[r-(1<<x)+1][x]);
}
int main(){
cin >> n >> q;
for(int i = 1; i <= n; i++) {
d[i] = read();
c[i] = read();
}
for(int i = 2; i <= n; i++) log_2[i] = log_2[i >> 1] + 1;
for(int i = 1; i <= n; i++) rmax[i][0] = d[i];
for(int j = 1; (1 << j) < n; j++)
for(int i = 1; i <= n - (1 << j) + 1; i++)
rmax[i][j] = max(rmax[i][j-1], rmax[i+(1 << j-1)][j-1]);
c[n+1] = inf;
for(int i = 1; i < n; i++){
int l = i + 1, r = n + 1, mid;
while(l < r){
mid = l + r >> 1;
if(querymax(i+1, mid) <= d[i]) l = mid + 1;
else r = mid;
}
f[i][0] = l;
g[i][0] = c[f[i][0]];
}
f[n][0] = n + 1;
g[n][0] = c[f[n][0]];
for(int t = 1; t <= 16; t++)
for(int i = 1; i <= n; i++){
f[i][t] = f[f[i][t-1]][t-1];
g[i][t] = g[i][t-1] + g[f[i][t-1]][t-1];
}
while(q--){
int r = read(), v = read();
if(v > c[r]){
v -= c[r];
for(int t = 16; t >= 0; t--)
if(v > g[r][t]){
v -= g[r][t];
r = f[r][t];
}
r = f[r][0];
}
if(r == n+1) r = 0;
printf("%d\n", r);
}
return 0;
}

浙公网安备 33010602011771号