【模板】数据结构:左偏树(可并堆) 、k 短路、最小树形图

左偏树

左偏树是一种支持可持久化和合并的二叉堆。记 \(dis_u\) 表示点 \(u\) 到最近的叶子的距离,那么左偏树时刻满足 \(dis_u=dis_{rc}+1\land dis_{lc}\geq dis_{rc}\) 且它的深度(\(dis_u\))是严格 \(O(\log n)\)

左偏树
template<int N,class T> struct leftree{
    int ch[N+10][2],dis[N+10],tot; T val[N+10];
    leftree():tot(0){dis[0]=-1;}
    int newnode(T x){int p=++tot;return val[p]=x,ch[p][0]=ch[p][1]=0,dis[p]=0,p;}
    int merge(int p,int q){
        if(!p||!q) return p+q;
        if(val[p].first>val[q].first) swap(p,q);
        ch[p][1]=merge(ch[p][1],q);
        if(dis[ch[p][0]]<dis[ch[p][1]]) swap(ch[p][0],ch[p][1]);
        dis[p]=dis[ch[p][1]]+1;
        return p;
    }
};

可持久化左偏树
template<int N,class T> struct leftree{
    int ch[N+10][2],dis[N+10],tot; T val[N+10];
    leftree():tot(0){dis[0]=-1;}
    int newnode(T x){int p=++tot;return val[p]=x,ch[p][0]=ch[p][1]=0,dis[p]=0,p;}
    int merge(int p,int q){
        if(!p||!q) return p+q;
        if(val[p].first-val[q].first>=eps) swap(p,q);
        int u=newnode(val[p]);
        ch[u][0]=ch[p][0],ch[u][1]=merge(ch[p][1],q);
        if(dis[ch[u][0]]<dis[ch[u][1]]) swap(ch[u][0],ch[u][1]);
        dis[u]=dis[ch[u][1]]+1;
        return u;
    }
};

左偏树可以支持全局做一些操作(全局加),和平衡树一样下传标记,记得删除节点的时候下放懒标记。

\(k\) 短路

\(k\) 短路问题输入一个有向带权图和整数 \(s,t,k\),要求找到这张图上 \(s\)\(t\) 的边权和第 \(k\) 小的路径。使用可持久化可并堆解决,时间复杂度为 \(O((n+m)\log n+k\log k)\)

考虑建出以 \(t\) 为根的反图的最短路树,满足每个点到 \(t\) 的树上路径是原图的(一条)最短路。考虑任意一条从 \(s\) 出发到 \(t\) 的路径,总是可以拆分成若干个形如 树上的儿子到祖先的路径 加上 一条非树边 的拼接。考虑假设现在只有一条非树边在里面,叫做 \((u,v,w)\),那么这条路径的长度是 \(dis_1-dis_u+w+dis_v\),如果还有一条边 \((u',v',w')\)\(v\) 的祖先上,那么就是 \(dis_1\boxed{-dis_u+w+dis_v}\boxed{-dis_{u'}+w'+dis_{v'}}\),也就是说一条边给路径长度的贡献是固定的,不妨叫做 \(\Delta e=dis_v-dis_u+w\) 好了。这里的 \(\Delta e\) 应该是个非负数。

考虑开始进行一波优先队列 bfs,我们对着一个状态,这个状态记录最后一条非树边走到了哪个点 \(u\)(或者 \(s\),如果没有走过)还有当前路径长度 \(cur\)(算上 \(u\)\(t\) 在树上那一段),那我就是爬 \(u\) 在树上的祖先,然后找一条出边 \((f,v,w)\) 出去,转移到 \((cur+\Delta e,v)\)。然后这个就是暴力,我们只要从优先队列里拿出 \(k\) 个状态,我们就说最后那一个是 \(k\) 短路。

考虑优化了,我们用左偏树,每个点存一个左偏树表示这个点和它的所有祖先伸出去的边,然后将优先队列状态改成记录左偏树节点编号,每次拿出状态之后,要么用掉这条边出去,要么将左偏树上两个儿子丢进优先队列,以后慢慢决策,反正左偏树是个小根堆,这样做没有问题。进一步的优化,就是把左偏树换成可持久化左偏树,将自己的左偏树和祖先的合并起来。就做完了。

点击查看代码

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr,##__VA_ARGS__)
#else
#define debug(...) void(0)
#endif
typedef long long LL;
const double eps=1e-7;
template<class T> using pqueue=priority_queue<T,vector<T>,greater<T>>;
template<int N,int M,class T=int> struct graph{
	int head[N+10],nxt[M<<1],cnt;
	struct edge{int u,v; T w;} e[M<<1];
	graph(){memset(head,cnt=0,sizeof head);}
	edge&operator[](int i){return e[i];}
	void add(int u,int v,T w=0){e[++cnt]={u,v,w},nxt[cnt]=head[u],head[u]=cnt;}
	void link(int u,int v,T w=0){add(u,v,w),add(v,u,w);}
};
template<int N,class T> struct leftree{
	int ch[N+10][2],dis[N+10],tot; T val[N+10];
	leftree():tot(0){dis[0]=-1;}
	int newnode(T x){int p=++tot;return val[p]=x,ch[p][0]=ch[p][1]=0,dis[p]=0,p;}
	int merge(int p,int q){
		if(!p||!q) return p+q;
		if(val[p].first-val[q].first>=eps) swap(p,q);
		int u=newnode(val[p]);
		ch[u][0]=ch[p][0],ch[u][1]=merge(ch[p][1],q);
		if(dis[ch[u][0]]<dis[ch[u][1]]) swap(ch[u][0],ch[u][1]);
		dis[u]=dis[ch[u][1]]+1;
		return u;
	}
};
int n,m;
double E,dis[5010];
graph<5010,1<<18,double> g,r;
void dijkstra(int s){
	pqueue<pair<double,int>> q;
	static bool vis[5010];
	fill(dis+1,dis+n+1,1e12);
	fill(vis+1,vis+n+1,0);
	for(q.push({dis[s]=0,s});!q.empty();){
		int u=q.top().second; q.pop();
		if(vis[u]) continue; else vis[u]=1;
		for(int i=r.head[u];i;i=r.nxt[i]){
			int v=r[i].v; double w=r[i].w;
			if(dis[v]-dis[u]-w>=eps) q.push({dis[v]=dis[u]+w,v});
		}
	}
}
int root[5010];
leftree<18<<18,pair<double,int>> t;
bool ontree[1<<18];
bool visit[5010];
void dfs(int u){
	visit[u]=1;
	for(int i=r.head[u];i;i=r.nxt[i]){
		int v=r[i].v; double w=r[i].w;
		if(!visit[v]&&fabs(dis[v]-dis[u]-w)<eps) dfs(v),ontree[i]=1;
	}
}
void pushdown(int u){
	for(int i=r.head[u];i;i=r.nxt[i]){
		int v=r[i].v; if(!ontree[i]) continue;
		root[v]=t.merge(root[v],root[u]);
		pushdown(v);
	}
}
int main(){
//	#ifdef LOCAL
//	 	freopen("input.in","r",stdin);
//	#endif
	scanf("%d%d%lf",&n,&m,&E);
	for(int i=1;i<=m;i++){
		int u,v; double w;
		scanf("%d%d%lf",&u,&v,&w);
		g.add(u,v,w),r.add(v,u,w);
	}
	dijkstra(n),dfs(n);
	for(int u=1;u<n;u++){
		if(!visit[u]) continue;
		for(int i=g.head[u];i;i=g.nxt[i]){
			int v=g[i].v; double w=g[i].w;
			if(ontree[i]) continue;
			root[u]=t.merge(
				root[u],
				t.newnode({w-dis[u]+dis[v],v})
			);
		}
	}
	pushdown(n);
	pqueue<pair<double,int>> q;
	int ans=0;
    auto update=[&](double w)->bool{
        if(E>=w) return E-=w,ans++,1;
        else return 0;
		debug("E=%.2lf,ans=%d\n",E,ans);
    };
	if(root[1]) q.push({dis[1]+t.val[root[1]].first,root[1]});
	if(update(dis[1])) while(!q.empty()&&E>=eps){
		double w=q.top().first; int u=q.top().second;
		debug("[%.2lf,%d]\n",w,u);
		q.pop();
        if(!update(w)) break;
		auto ins=[&](double del,int v){if(v) q.push({w+del,v});};
		ins(t.val[t.ch[u][0]].first-t.val[u].first,t.ch[u][0]);
		ins(t.val[t.ch[u][1]].first-t.val[u].first,t.ch[u][1]);
		int v=t.val[u].second;
		ins(t.val[root[v]].first,root[v]);
	}
	printf("%d\n",ans);
	return 0;
}


朱刘算法求最小树形图

为了解决最小树形图,问题我们有如下做法:(抄的题解,是父亲指向儿子的)

  • 考虑树形图的性质,每个点都有唯一的入边(根节点除外,以后讨论入边,都不考虑根节点),于是我们提出这样一个算法,强制选择每个点边权最小的入边,这样如果不存在环,我们肯定得到了最小的树形图。
  • 考虑如何处理环,有一条性质,因为这个环是由最小的入边所形成的环,因此存在一棵最小树形图,只缺少了环上的一条边,而且缺少的这条边所指向的点的入边必在该棵最小树形图上。
  • 我们想要这个环缩成一个点 \(cnt\),而且要表现环上的每条边选与不选,对于进入环上的每条边 \((u,v,w)\)\(v\) 为环上的点,\(u\) 非环上的点,令 \(w−=in[v]\)\(in[v]\)\(v\) 现在的最小入边),然后 \(v=cnt\),答案强制选上环上的边权,然后删除环上所有的内部连边,把这个环缩成一个点,递归进行。
  • 每次形成一个环会至少少一个点,时间复杂度 \(O(nm)\)

这就差不多是一个反悔贪心。

tarjan 优化朱刘算法

复杂度的瓶颈在于缩环和维护 \(in_v\),用左偏树(可并堆)维护。

  • 枚举原图上的边 \((pre_u,u,in_u)\),加入图中。
  • 如果此时图中有有向环,找出这个环。
  • 对每个点维护左偏树表示入边权值。合并左偏树的时候,先是计算贡献,然后点 \(u\) 的左偏树的权值集体减 \(in_u\),合并环上左偏树到一个新的点上。
点击查看代码

输入要求内向树形图,swap(u,v) 之后变成每个点只有一条出边(根没有),跑上面的缩环算法,最后看一下是否除了根其他点都有一条出边,那么选他就是了。

 
#include <queue>
#include <numeric>
#include <cstdio>
#include <vector>
#include <cstring>
#include <cassert>
#include <algorithm>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr,##__VA_ARGS__)
#else
#define debug(...) void(0)
#endif
typedef long long LL;
template<int N> struct dsu{
    int fa[N+10],siz[N+10],cnt;
    explicit dsu(int n=N):cnt(n){iota(fa+1,fa+n+1,1),fill(siz+1,siz+n+1,1);}
    int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
    void merge(int x,int y){if(x=find(x),y=find(y),x!=y) cnt--,fa[y]=x,siz[x]+=siz[y];}
};
template<int N> struct leftree{
    int ch[N+10][2],dist[N+10],cnt;
    pair<LL,int> val[N+10]; LL tag[N+10];
    leftree():cnt(0){dist[0]=-1;}
    int newnode(pair<LL,int> v){
        int p=++cnt;
        val[p]=v,tag[p]=0;
        ch[p][0]=ch[p][1]=0,dist[p]=0;
        return p;
    }
    void spread(int p,LL k){val[p].first+=k,tag[p]+=k;}
    void pushdown(int p){spread(ch[p][0],tag[p]),spread(ch[p][1],tag[p]),tag[p]=0;}
    int merge(int p,int q){
        if(!p||!q) return p+q;
        pushdown(p),pushdown(q);
        if(val[p]>val[q]) swap(p,q);
        ch[p][1]=merge(ch[p][1],q);
        if(dist[ch[p][0]]<dist[ch[p][1]]) swap(ch[p][0],ch[p][1]);
        dist[p]=dist[ch[p][1]]+1;
        return p;
    }
};
int n,m,k,root[1<<18],rt;
leftree<1<<18> t;
pair<LL,int> to[1<<18];
dsu<1<<18> cyc,idp;
int solve(){
    int ans=0;
    auto top=[&](int x){return t.val[x];};
    auto pop=[&](int&p){t.pushdown(p),p=t.merge(t.ch[p][0],t.ch[p][1]);};
    auto getto=[&](int x){
        auto res=top(root[x]);
        while(root[x]&&idp.find(res.second)==idp.find(x)){
            pop(root[x]);
            res=top(root[x]);
        }
        return root[x]?res:make_pair(0ll,0);
    };
    queue<int> q;
    for(int i=1;i<=n;i++) to[i]=getto(i);
    for(int i=1;i<=n;i++) if(to[i].second) q.push(i);
    int cnt=n;
    while(!q.empty()){
        int u=q.front(),v=to[u].second; q.pop();
        ans+=to[u].first;
        vector<int> cycle;
        if(cyc.find(u)==cyc.find(v)){
            assert(idp.find(u)==u);
            cycle.push_back(u);
            for(int p=idp.find(to[u].second);p!=u;p=idp.find(to[p].second)){
                cycle.push_back(p);
            }
        }else{cyc.merge(v,u);continue;}
        idp.siz[++cnt]=0;
        for(int p:cycle){
            idp.merge(cnt,p),cyc.merge(cnt,p);
            t.spread(root[p],-to[p].first);
            root[cnt]=t.merge(root[cnt],root[p]);
        }
        to[cnt]=getto(cnt);
        if(to[cnt].second) q.push(cnt);
    }
    for(int i=1;i<=cnt;i++) if(idp.find(i)==i&&idp.find(i)!=idp.find(rt)&&!to[i].second) return -1;
    return ans;
}
int main(){
    scanf("%d%d%d",&n,&m,&rt);
    for(int i=1,u,v,w;i<=m;i++){
        scanf("%d%d%d",&u,&v,&w); swap(u,v);
        if(u!=rt) root[u]=t.merge(root[u],t.newnode({w,v}));
    }
    printf("%d\n",solve());
    return 0;
}
 
 

posted @ 2023-09-11 19:28  caijianhong  阅读(27)  评论(0编辑  收藏  举报