【瞎口胡】线段树分治
线段树分治是一种优秀的时间轴分治算法。
例题 1 Luogu P5787 二分图
题意
有一张 \(n\) 个点 \(m\) 条边的无向图,第 \(i\) 条边在 \(x\) 时刻出现 \(y\) 时刻消失,询问时间段 \(1 \sim k\) 这张图是不是二分图。
时间段 \(x\) 的定义是时刻 \(x-1\) 到 \(x\) 这一段时间。
\(1 \leq n,k \leq 10^5, m \leq 2 \times 10^5,1 \leq x,y \leq k\)
题解
容易观察到,在这道题中,我们要维护的信息易加不易减。线段树分治可以很好地避免这个问题。
对时间建线段树,一条边的出现和消失就是区间修改。每个节点挂一个 std::vector
就行。
假设当前要询问的时间是 \(x\)。在线段树上从根节点往下走到 \(x\) 对应的叶节点,并将沿途经过所有节点中存下的边加入图中。
用扩展域并查集维护。处理一条边 \((u,v)\) 时,在并查集中将点 \((u,v')\) 和 \((v,u')\) 连边。如果某个点 \(i\) 和 \(i'\) 在并查集中连通,则该图不是二分图。这是因为二分图的判定定理:一个图是二分图当且仅当图中不存在奇环。考虑一条边一定从不带 \('\) 的点连向带 \('\) 的点,如果 \(i\) 和 \(i'\) 在并查集中连通,那么一定经过了奇数条边,这样图中就存在一个奇环;反之亦然。
但我们不能直接进行 \(k\) 次查询:每次查询需要清空数组,复杂度退化为了 \(O(kn)\)。如果使用可撤销并查集就可以解决这一问题。具体来讲,在并查集连边时将这一条边两个端点连边前的 \(fa\) 记录下来,存放在栈中,在退出当前节点时弹栈复原信息即可。同时,我们查询完当前节点的左子树时不退出,而是直接进入右子树查询。
时间复杂度 \(O(m \log k \log n)\)。
# include <bits/stdc++.h>
const int N=100010,INF=0x3f3f3f3f;
struct Node{
int u,sizeu,v,sizev;
};
typedef std::pair <int,int> pii;
pii edge[N<<1];
std::vector <int> tree[N<<2];
int n,m,tlim;
int f[N<<1],size[N<<1];
std::stack <Node> sta;
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
inline int lc(int x){
return x<<1;
}
inline int rc(int x){
return x<<1|1;
}
inline int geta(pii x){
return x.first;
}
inline int getb(pii x){
return x.second;
}
void change(int k,int l,int r,int L,int R,int x){
if(L<=l&&r<=R){
tree[k].push_back(x);
return;
}
int mid=(l+r)>>1;
if(L<=mid)
change(lc(k),l,mid,L,R,x);
if(mid<R)
change(rc(k),mid+1,r,L,R,x);
return;
}
inline int find(int x){
return (f[x]==x)?x:find(f[x]);
}
inline void Union(int u,int v){
u=find(u),v=find(v);
if(size[u]>size[v])
std::swap(u,v);
sta.push((Node){u,size[u],v,size[v]});
f[u]=v,size[v]+=size[u];
return;
}
void calc(int k,int l,int r){
bool flag=true;
int now=sta.size();
for(int i=0;i<(int)tree[k].size();++i){
int u=geta(edge[tree[k][i]]),v=getb(edge[tree[k][i]]);
if(find(u)==find(v)){
for(int p=l;p<=r;++p)
puts("No");
flag=false;
break;
}
Union(u,v+n),Union(v,u+n);
}
int mid=(l+r)>>1;
if(flag){
if(l==r)
puts("Yes");
else
calc(lc(k),l,mid),calc(rc(k),mid+1,r);
}
while((int)sta.size()>now){
Node val=sta.top();
sta.pop();
f[val.u]=val.u,f[val.v]=val.v,size[val.u]=val.sizeu,size[val.v]=val.sizev;
}
return;
}
int main(void){
n=read(),m=read(),tlim=read();
for(int i=1;i<=2*n;++i){
f[i]=i,size[i]=1;
}
for(int i=1,l,r;i<=m;++i){
edge[i].first=read(),edge[i].second=read();
l=read(),r=read();
if(l!=r)
change(1,1,tlim,l+1,r,i);
}
calc(1,1,tlim);
return 0;
}
例题 2 Luogu P4141 消失之物
题意
有一个 \(n\) 个物品和一个背包。问对于每一个物品,其消失后装满容量为 \(1 \sim m\) 的背包的方案数。
\(1 \leq n,m \leq 2000\)
题解
可以看成是物品 \(i\) 在 \([1,i-1]\) 和 \([i+1,n]\) 出现。
当然,有一种更简便的写法:查询左区间就把右区间的所有物品加入背包,查询结束之后撤销。查询右区间同理。
# include <bits/stdc++.h>
const int N=2010,INF=0x3f3f3f3f;
int f[20][N];
int a[N];
int n,m;
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
void solve(int l,int r,int dep){
if(l==r){
for(int i=1;i<=m;++i){
printf("%d",f[dep][i]);
}
puts("");
return;
}
int mid=(l+r)>>1;
for(int i=0;i<=m;++i)
f[dep+1][i]=f[dep][i];
for(int i=mid+1;i<=r;++i){
for(int j=m;j>=a[i];--j){
f[dep+1][j]=(f[dep+1][j]+f[dep+1][j-a[i]])%10;
}
}
solve(l,mid,dep+1);
for(int i=0;i<=m;++i)
f[dep+1][i]=f[dep][i];
for(int i=l;i<=mid;++i){
for(int j=m;j>=a[i];--j){
f[dep+1][j]=(f[dep+1][j]+f[dep+1][j-a[i]])%10;
}
}
solve(mid+1,r,dep+1);
return;
}
int main(void){
n=read(),m=read();
for(int i=1;i<=n;++i){
a[i]=read();
}
f[1][0]=1;
solve(1,n,1);
return 0;
}