7.17复习笔记
一、树套树
线段树套线段树
1.实现原理:
我们考虑用树套树如何实现在二维平面上进行单点修改,区域查询。我们考虑外层的线段树,最底层的1到n个节点的子树,分别代表第1到第n行的线段树。那么这些底层的节点对应的父节点,就代表其两个子节点的子树所在的一片区域。
2.空间复杂度
通常情况下,我们不可能对于外层线段树的每一个结点都建立一颗子线段树,空间需求过大。树套树一般采取动态开点的策略。单次修改,我们会涉及到外层线段树的\(logn\)个节点,且对于每个节点的子树涉及\(log\)个节点,所以单次修改产生的空间最多为\(log^2n\)。
3.时间复杂度
对于询问操作,我们考虑我们在外层线段树上进行\(logn\)次操作,每次操作会在一个内层线段树上进行\(logn\)次操作,所以时间复杂度为\(log^2n\)。 修改操作,与询问操作复杂度相同,也为\(log^2n\)。
例题:三维偏序(陌上花开)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define B cout<<"Breakpoint"<<endl;
#define O(x) cout<<#x<<" "<<x<<endl;
#define o(x) cout<<#x<<" "<<x<<" ";
using namespace std;
int read(){
int x = 1,a = 0;char ch = getchar();
while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
return x*a;
}
const int maxn = 5e5+10, inf = 1e9+7;
int n,k;
struct node{
int a,b,c;
}arr[maxn];
bool cmp(node x,node y){
return x.a < y.a;
}
int tot;
struct SEGTree{
int l,r,sum;
}t[maxn * 30];
int ls(int x){return x << 1;}
int rs(int x){return x << 1 | 1;}
int root[maxn * 30];
void modifyIn(int &x,int l,int r,int p){
if (!x) x = ++tot;
t[x].sum ++;
if (l == r) return;
int mid = (l+r >> 1);
if (p <= mid) modifyIn(t[x].l,l,mid,p);
else modifyIn(t[x].r,mid+1,r,p);
}
void modifyOut(int x,int l,int r,int p1,int p2){
modifyIn(root[x],1,k,p2);
if (l == r) return;
int mid = (l+r >> 1);
if (p1 <= mid) modifyOut(ls(x),l,mid,p1,p2);
else modifyOut(rs(x),mid+1,r,p1,p2);
}
int queryIn(int x,int l,int r,int nl,int nr){
int res = 0;
if (nl <= l&&r <= nr) return t[x].sum;
int mid = (l+r >> 1);
if (nl <= mid) res += queryIn(t[x].l,l,mid,nl,nr);
if (nr > mid) res += queryIn(t[x].r,mid+1,r,nl,nr);
return res;
}
int queryOut(int x,int l,int r,int l1,int r1,int l2,int r2){
if (l1 <= l&&r <= r1) return queryIn(root[x],1,k,l2,r2);
int res = 0,mid = (l+r >> 1);
if (l1 <= mid) res += queryOut(ls(x),l,mid,l1,r1,l2,r2);
if (r1 > mid) res += queryOut(rs(x),mid+1,r,l1,r1,l2,r2);
return res;
}
void debug(int x,int l,int r){
if (l == r) return;
int mid = (l+r >> 1);
debug(ls(x),l,mid),debug(rs(x),mid+1,r);
}
int ans[maxn];
int main(){
n = read(),k = read();
for (int i = 1;i <= n;i++) arr[i].a = read(),arr[i].b = read(),arr[i].c = read();
sort(arr+1,arr+n+1,cmp);
int lst = 1;
modifyOut(1,1,k,arr[1].b,arr[1].c);
for (int i = 2;i <= n;i++){
if (arr[i].a != arr[lst].a){
for (int j = lst;j < i;j++){
// cout<<j<<" "<<arr[j].a<<" "<<arr[j].b<<" "<<arr[j].c<<" "<<queryOut(1,1,k,1,arr[j].b,1,arr[j].c)-1<<endl;
ans[queryOut(1,1,k,1,arr[j].b,1,arr[j].c)-1] ++;
}
lst = i;
}
modifyOut(1,1,k,arr[i].b,arr[i].c);
}
for (int i = lst;i <= n;i++) ans[queryOut(1,1,k,1,arr[i].b,1,arr[i].c)-1]++;
for (int i = 0;i < n;i++) printf("%d\n",ans[i]);
}
树状数组套主席树
静态区间k小值的问题可以用主席树在\(O(nlogn)\)的时间复杂度内解决。
如果区间变成动态的呢?即,如果还要求支持一种操作:单点修改某一位上的值,又该怎么办呢?
修改操作进行时,先在线段树上从上往下跳到被修改的点,删除所经过的点所指向的动态开点权值线段树上的原来的值,然后插入新的值,要经过\(O(logn)\)个线段树上的节点,在动态开点权值线段树上一次修改操作是 的\(O(logn)\),所以修改操作的时间复杂度为\(O(log^2n)\)
在查询答案时,先取出该区间覆盖在线段树上的所有点,然后用类似于静态区间k小值的方法,将这些点一起向左儿子或向右儿子跳。如果所有这些点左儿子存储的值大于等于k,则往左跳,否则往右跳。由于最多只能覆盖\(O(logn)\)个节点,所以最多一次只有这么多个节点向下跳,时间复杂度为\(O(log^2n)\)
由于线段树的常数较大,在实现中往往使用常数更小且更方便处理前缀和的树状数组实现。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define B cout<<"Breakpoint"<<endl;
#define O(x) cout<<#x<<" "<<x<<endl;
#define o(x) cout<<#x<<" "<<x<<" ";
using namespace std;
int read(){
int x = 1,a = 0;char ch = getchar();
while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
return x*a;
}
const int maxn = 2e5+10;
int n,m,a[maxn],len;
struct SEGTree{
int l,r,sum;
}t[maxn * 30];
struct node{
int l,r,op,k;
}q[maxn];
int arr1[maxn],arr2[maxn],cnt1,cnt2;
int root[maxn],tot;
void modifyIn(int &x,int l,int r,int p,int k){
if (!x) x = ++tot;
t[x].sum += k;
if (l == r) return;
int mid = (l+r >> 1);
if (p <= mid) modifyIn(t[x].l,l,mid,p,k);
else modifyIn(t[x].r,mid+1,r,p,k);
}
void modifyOut(int x,int p,int k){
for (int i = x;i <= n;i += i&-i) modifyIn(root[i],1,len,p,k);
}
int query(int l,int r,int k){
int res = 0,mid = (l+r >> 1);
if (l == r) return l;
for (int i = 1;i <= cnt1;i++) res -= t[t[arr1[i]].l].sum;
for (int i = 1;i <= cnt2;i++) res += t[t[arr2[i]].l].sum;
if (res >= k){
for (int i = 1;i <= cnt1;i++) arr1[i] = t[arr1[i]].l;
for (int i = 1;i <= cnt2;i++) arr2[i] = t[arr2[i]].l;
return query(l,mid,k);
}
else{
for (int i = 1;i <= cnt1;i++) arr1[i] = t[arr1[i]].r;
for (int i = 1;i <= cnt2;i++) arr2[i] = t[arr2[i]].r;
return query(mid+1,r,k-res);
}
}
int kth(int l,int r,int k){
cnt1 = cnt2 = 0;
for (int i = l-1;i;i -= i&-i) arr1[++cnt1] = root[i];
for (int i = r;i;i -= i&-i) arr2[++cnt2] = root[i];
return query(1,len,k);
}
int b[maxn],cnt;
int main(){
n = read(),m = read();
for (int i = 1;i <= n;i++) a[i] = b[++cnt] = read();
for (int i = 1;i <= m;i++){
char op[5];scanf ("%s",op);
if (op[0] == 'Q') q[i].op = 1,q[i].l = read(),q[i].r = read(),q[i].k = read();
if (op[0] == 'C') q[i].op = 2,q[i].l = read(),q[i].k = b[++cnt] = read();
}
sort(b+1,b+cnt+1);
len = unique(b+1,b+cnt+1)-b-1;
for (int i = 1;i <= n;i++) a[i] = lower_bound(b+1,b+len+1,a[i])-b;
for (int i = 1;i <= m;i++){
if (q[i].op == 2) q[i].k = lower_bound(b+1,b+len+1,q[i].k)-b;
}
for (int i = 1;i <= n;i++) modifyOut(i,a[i],1);
for (int i = 1;i <= m;i++){
if (q[i].op == 1) printf("%d\n",b[kth(q[i].l,q[i].r,q[i].k)]);
else modifyOut(q[i].l,a[q[i].l],-1),modifyOut(q[i].l,q[i].k,1),a[q[i].l] = q[i].k;
}
return 0;
}
线段树套平衡树
平衡树可以被值域线段树替代,唯一需要注意的就是如何在值域线段树上查找前驱和后继了
思想都是差不多的,一个数的前驱对应的是值域线段树上包含这个点的区间的左子树的右子树的右子树……
如果右子树没有值了,我们再去查询左子树的右子树的右子树……
后继相同
int preIn(int x,int l,int r,int p){
if (!t[x].sum) return 0;
if (l == r) return l;
int mid = (l+r >> 1);
if (p <= mid) return preIn(t[x].l,l,mid,p);
int tmp = preIn(t[x].r,mid+1,r,p);
if (tmp) return tmp;
return preIn(t[x].l,l,mid,p);
}
int preOut(int x,int l,int r,int nl,int nr,int k){
int res = 0;
if (nl <= l&&r <= nr) return preIn(root[x],1,len,k);
int mid = (l+r >> 1);
if (nl <= mid) res = max(res,preOut(ls(x),l,mid,nl,nr,k));
if (nr > mid) res = max(res,preOut(rs(x),mid+1,r,nl,nr,k));
return res;
}
int nxtIn(int x,int l,int r,int p){
if (!t[x].sum) return 2147483647;
if (l == r) return l;
int mid = (l+r >> 1);
if (p > mid) return nxtIn(t[x].r,mid+1,r,p);
int tmp = nxtIn(t[x].l,l,mid,p);
if (tmp != 2147483647) return tmp;
return nxtIn(t[x].r,mid+1,r,p);
}
int nxtOut(int x,int l,int r,int nl,int nr,int k){
int res = 2147483647;
if (nl <= l&&r <= nr) return nxtIn(root[x],1,len,k);
int mid = (l+r >> 1);
if (nl <= mid) res = min(res,nxtOut(ls(x),l,mid,nl,nr,k));
if (nr > mid) res = min(res,nxtOut(rs(x),mid+1,r,nl,nr,k));
return res;
}
分块套树状数组
二、平衡树
因为上面提到了线段树套平衡树,顺手就把无旋Treap的核心代码背一下吧
treap有两个属性,价值和随机生成数,分别满足二叉查找树和堆的性质
其他的一些操作就可以不断的通过分裂和合并或者在平衡树上走一遍求解
void split(int now,int k,int &x,int &y){
if (!now){x = y = 0;return;}
if (siz[ch[now][0]] < k){
x = now;
split(ch[now][1],k-siz[ch[now][0]]-1,ch[now][1],y);
}
else{
y = now;
split(ch[now][0],k,x,ch[now][0]);
}
pushup(now);
}
int merge(int x,int y){
if (!x || !y) return x+y;
if (rd[x] < rd[y]){
ch[x][1] = merge(ch[x][1],y);
pushup(x);
return x;
}
else{
ch[y][0] = merge(x,ch[y][0]);
pushup(y);
return y;
}
}
三、分块
分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。
分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。
求区间众数
序列分块预处理出两两块间的众数,然后再查询散块内的数能否成为区间众数
至于怎么求一个数再区间出现的次数呢??对每个数开一个vector,vector里二分找第一个大于r的位置-第一个大于等于的位置
区间加,区间大于等于一个数的数的个数
可以参照线段树区间加的方式对整块打上标记对散块暴力修改
每次查询对于在整块的vector里二分,散块暴力查询
区间出现次数为偶数的数的个数
和寻找区间众数差不多,也是预处理出两两块之间的出现次数为偶数的数的个数
对于散块暴力查询看他是否会对整块的数产生影响
区间加等差数列,区间求和
如果是单点修改,直接线段树即可对付。但是对于区间修改,线段树就无法胜任了。
因为等差数列加等差数列还是还是一个等差数列,所以对每一个块维护等差数列的首项beg以及公差d
这样,一个块内所有点的值都可以写成与下标相关的一次函数的形式
即\(ans[i]=i \times d+sum[i],sum[i]=ans[i]-i \times d\)
其中sum[i] 为上一次重构后i处前缀和的值
对于整个块的首项,我们先不去考虑,最后把它加上即可
这样,我们就可以把下标i看成横坐标,把sum[i]看成纵坐标,把−d看成斜率
如果是对整个块进行修改,那么横纵坐标都是不变的,变化的只是斜率,因此可以维护一个上凸壳
在上凸壳中二分查找使截距 ans[i] 最大的点
如果是对块的一部分进行修改或查询,我们就把这个块进行暴力重构,重新建一个凸包
四、网络流
最大流
bool BFS(){
queue<int> q;q.push(s);
memset(dis,0,sizeof(dis));
dis[s] = 1;
while (!q.empty()){
int x = q.front();q.pop();
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to]||!ed[i].w) continue;
dis[to] = dis[x]+1;
q.push(to);
}
}
return dis[t];
}
int cur[maxn];
int DFS(int x,int lim){
if (!lim||x == t) return lim;
int res = 0;
for (int &i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to] != dis[x]+1) continue;
int tmp = DFS(to,min(lim,ed[i].w));
lim -= tmp,ed[i].w -= tmp,ed[i^1].w += tmp,res += tmp;
if (!lim) break;
}
return res;
}
signed main(){
n = read(),m = read(),s = read(),t = read();
for (int i = 1;i <= m;i++){
int u = read(),v = read(),w = read();
add(u,v,w),add(v,u,0);
}
for (int i = 0;i <= tot;i++) cur[i] = head[i];
int tmp,ans = 0;
while (BFS()){
while (tmp = DFS(s,inf)) ans += tmp;
for (int i = 0;i <= tot;i++) head[i] = cur[i];
}
cout<<ans<<endl;
return 0;
}
费用流
bool SPFA(){
queue<int> q;q.push(s);
for (int i = s;i <= t;i++) dis[i] = inf,vis[i] = 0;
dis[s] = 0,vis[s] = 1;
while (!q.empty()){
int x = q.front();q.pop();
vis[x] = 0;
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (!ed[i].lim) continue;
if (dis[to] > dis[x]+ed[i].w){
dis[to] = dis[x]+ed[i].w;
pre1[to] = x,pre2[to] = i;
if (!vis[to]){
vis[to] = 1;
q.push(to);
}
}
}
}
return dis[t] != inf;
}
signed main(){
n = read(),m = read(),s = 1,t = n;
for (int i = 1;i <= m;i++){
int u = read(),v = read(),lim = read(),w = read();
add(u,v,lim,w),add(v,u,0,-w);
}
int ans1 = 0,ans2 = 0;
while (SPFA()){
int tmp = inf;
for (int i = t;i != s;i = pre1[i]) tmp = min(tmp,ed[pre2[i]].lim);
ans1 += tmp,ans2 += dis[t]*tmp;
for (int i = t;i != s;i = pre1[i]) ed[pre2[i]].lim -= tmp,ed[pre2[i]^1].lim += tmp;
}
cout<<ans1<<" "<<ans2<<endl;
return 0;
}
一些模型:
最小点覆盖
最小点覆盖:选取最少的点,使得所有的点与至少一个被选取点距离不超过1,或选取最少的点,使得所有边都至少与一个被选取点相连
方法:最小点覆盖=最大匹配
最大独立集
最大独立集:选取最多的点,使得其中任意两点互不相达
方法:最大独立集 = 点总数 - 最大匹配
最小边覆盖
最小边覆盖:选取最少的边使得所有的点被至少一条边覆盖
方法:最小边覆盖 = 总点数 - 最大匹配