这周按照上周的安排学习线段树,线段树区别于CDQ分治,CDQ 分治的核心思想就是将修改和询问丢一块,然后左右区间合并上来的时候考虑左边区间对右边区间的贡献。而 CDQ 分治处理的修改一般都是永久性的,不可撤销。
有时,进行的一种操作可以快速完成,但这种操作的逆操作难以实现。比如,维护一些连通性,或直径等问题。这类问题加边很好做,但删边很难实现。但是若题目中需要加边,删边,查询,且支持离线,可以采用如下做法:线段树分治,将每个添加操作的有效区间按在线段树上,然后遍历这颗线段树同时处理标记即可。
而一般来说,需要利用线段树解题都有其特征,一般都是给你若干操作,每一个操作的作用时间范围为 [l,r],然后让你求每一个时刻下 (题目要求) 的结果,或是没有说明操作时间,我们以操作个数为时间,当某些操作完全相同,则可以合并。这样的话如果进行询问,只需要DFS就可以。而且关于线段树,我看的博客里也基本都是题目的集合,以题为主,学习线段树也要基于题目,关于优化建树方面,这个dalao的博客写的很详细另外就是板子,跟着博客复现:
#include<bits/stdc++.h>
#define INF 0x7fffffff
const int N=200010;
using namespace std;
inline int read(){
char ch=getchar();int x=0,f=1;
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
//180W点 520W条边 n+m 20W
struct edge{
int v,nxt,w;
}e[N*40];
int head[N<<4],cnt;
long long inf;
int n,m,s;
void add(int u,int v,int w){
e[++cnt].v=v;
e[cnt].w=w;
e[cnt].nxt=head[u];
head[u]=cnt;
}
int tin[N<<4],tout[N<<4],temp;
long long dis[N<<4];
void build(int k,int l,int r){
if(l==r){
tin[k]=tout[k]=l;
return ;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
tin[k]=++temp;tout[k]=++temp;
add(tout[k<<1],tout[k],0);
add(tout[k<<1|1],tout[k],0);
add(tin[k],tin[k<<1],0);
add(tin[k],tin[k<<1|1],0);
}
void update1(int k,int l,int r,int L,int R,int u,int w){
if(L<=l&&r<=R){
add(u,tin[k],w);
return ;
}
int mid=(l+r)>>1;
if(mid>=L){
update1(k<<1,l,mid,L,R,u,w);
}
if(mid<R){
update1(k<<1|1,mid+1,r,L,R,u,w);
}
}
void update2(int k,int l,int r,int L,int R,int u,int w){
if(L<=l&&r<=R){
add(tout[k],u,w);
return ;
}
int mid=(l+r)>>1;
if(mid>=L){
update2(k<<1,l,mid,L,R,u,w);
}
if(mid<R){
update2(k<<1|1,mid+1,r,L,R,u,w);
}
}
struct node{
int u;
long long d;
bool operator<(const node&h)const{return d>h.d;};
};
void dij(){
memset(dis,0x3f,sizeof(dis));
inf=dis[1];
priority_queue<node> q;
dis[s]=0;
q.push((node){s,0});
while(!q.empty()){
node now=q.top();
int u=now.u;
long long d=now.d;
q.pop();
if(d!=dis[u])continue;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push((node){v,dis[v]});
}
}
}
}
int main(){
n=read(),m=read(),s=1;
temp=n+m;
build(1,1,n+m);
for(int i=1;i<=m;i++){
int l=read(),r=read(),w=read();
update1(1,1,n+m,l,r,n+i,0);
update2(1,1,n+m,l,r,n+i,w);
}
dij();
//cout<<inf<<endl;
if(dis[n]!=inf){
printf("%lld ",dis[n]);
}
else printf("-1 ");
return 0;
}
这里有一个紫题的模版题,题目可以看作,给你 M个操作,每次在时间 [l,r]内加上一条边,并且这里每一条边都是不同的。符合使用线段树的条件,这里贴部分关键操作的代码。
const int MAX = 1e5+7;
typedef pair<int, int> pii;
struct UnionFind {
private:
int rk[MAX << 1], pre[MAX << 1], totNode;
stack<pii> st;//记录操作
public:
void init(int tot) {
totNode = tot;
for (int i = 1; i <= totNode * 2; i++)//将种类并查集分成两类
pre[i] = i, rk[i] = 1;
}
int find(int x)
{
while (x ^ pre[x]) //^运算可加快代码速度
x = pre[x];
return x;
}
void merge(int x, int y) //开始按秩合并
{
x = find(x), y = find(y);
if (x == y)
return;
if (rk[x] < rk[y])
swap(x, y);
st.push(make_pair(y, rk[x]));//将操作存在栈中
pre[y] = x, rk[x] += rk[x] == rk[y];
}
int start()
{ return st.size(); }//当前点开始时栈中操作数
void end(int last) //撤回merge操作
{
while (st.size() > last)
{//一直到最开始为止
pii tp = st.top();
//这里按之前合并的时候反过来写就行了
rk[pre[tp.first]] -= tp.second;
pre[tp.first] = tp.first;
st.pop();
}
}
} uf;
void insert(int u, int l, int r, int ql, int qr, pii k) {//将加入的边插入[ql, qr]时间段即可
if (ql <= l && r <= qr) {
t[u].push_back(k);
return;
}
if (ql <= m)
insert(lc, l, m, ql, qr, k);
if (qr > m)
insert(rc, m + 1, r, ql, qr, k);
}
void dfs(int u, int l, int r) {
int now = uf.start(), flag = 0;//now记录最开始栈中元素个数
for (auto &i: t[u])
{
int x = i.first, y = i.second;
//将x和y分成两种不同的类
if (uf.find(x) == uf.find(y)) //如果x和y已经是同类了, 那么就有冲突了
{
flag = 1;
break;
}
//将x和y分成两种不同的类
uf.merge(x, y + N); uf.merge(y, x + N);
}
if (flag)
for (int i = l; i <= r; i++)
cout<<"no"<<endl;//有冲突,那么显然他的子树都不用在看了,肯定有冲突了
else if (l == r)
cout<<"yes"<<endl;//如果到达子树都没有冲突,说明可以
else
dfs(lc, l, m), dfs(rc, m + 1, r);
uf.end(now);//每次结束都撤回操作
}
转眼11月竟然快过去了,总结一下这个月都码了什么、学了什么。新的算法知识主要是莫队算法,CQD分治、主席树和线段树,另外就是写了一些最短路之类的题,还有复习基本的算法知识用Java实现,进行一个知识迁移。下周继续补充线段树的题库,线段树、主席树之类的题目还真得多看题,才能在遇到的时候能想到是用这钟方式来解决。