集训D4-5

DAT4-5 图论

最短路

性质

dis[u]代表从源点走到u的最短路长度

1.贪心性:源点到任意一个点最短路上的每一步都是一个最短路

2.存在性:两个点之间的最短路有可能不存在。(源点存在一个到达该点且经过一个负环的路径/图不连通)

3.三角形不等式:对于一条边uwv,一定有dis[v]<=dis[u]+w

4.最短路图:从源点出发,把所有dis[v]=dis[u]+w的边建出来可以得到一张DAG。(不考虑0环)

算法

松弛操作:对于一条边,若当前dis[v]>dis[u]+w,则置dis[v]=dis[u]+w

1.Bellman-Ford(SPFA)

O(nm)

2.Dijkstra

朴素O(n2)

堆优化O((n+m)log(n+m))

只用于正权图

3.Floyd

O(n3)

4.Johnson

用Dijkstra跑全源最短路

解决负权边:增加点权h[u],将边的权值由w改为w+h[u]h[v]

这样不会影响最短路大小比较

h[u]满足性质:w+h[u]h[v]>=0,即h[v]<=w+h[u]

建立一个超级源点(防止有点遍历不到)跑Bellman-Ford,得到h[u]

O(nmlogm)

bool check(){
	for(int i=1;i<=n;i++)h[i]=1e9;
	queue<int>q;
	q.push(0);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		for(int j=0;j<a[u].size();j++){
			int v=a[u][j].v,w=a[u][j].w;
			if(h[v]>h[u]+w){
				h[v]=h[u]+w;
				q.push(v);
				in[v]++;
				if(in[v]>n+1)return 1;//判断负环,加上超级源点共n+1个点
			}
		}
	}
	return 0;
}
void dijkstra(int s){
	memset(vis,0,sizeof vis);
	for(int i=1;i<=n;i++)d[i]=1e9;
	d[s]=0;
	priority_queue<node>q;
	q.push(node{s,0});
	while(!q.empty()){
		node now=q.top();
		q.pop();
		if(vis[now.u])continue;
		else vis[now.u]=1;
		for(int j=0;j<a[now.u].size();j++){
			int v=a[now.u][j].v,w=a[now.u][j].w;
			if(d[v]>d[now.u]+w+h[now.u]-h[v]){
				d[v]=d[now.u]+w+h[now.u]-h[v];
				q.push(node{v,d[v]});
			}
		}
	}
}
for(int i=1;i<=n;i++)a[0].push_back(edge{i,0});//建立超级源点
for(int i=1;i<=n;i++)dijkstra(i);//统计答案时特判路径不存在的情况

问题

1.输出方案

pre[i]记录前驱节点

2.传递闭包

bitset优化O(n3w)

3.选择k条道路以c代价通行,求最短路

分层图 O(kmlogm)

for(int i=1;i<=m;i++){//P4568 P4822
    int u=read()+1,v=read()+1,w=read();	
    a[u].push_back(edge{v,w});
    a[v].push_back(edge{u,w});
    for(int j=1;j<=k;j++){
        a[u+j*n].push_back(edge{v+j*n,w});
        a[v+j*n].push_back(edge{u+j*n,w});
        a[u+(j-1)*n].push_back(edge{v+j*n,0});
        a[v+(j-1)*n].push_back(edge{u+j*n,0});
    }
}

4.给出一个N个顶点M条边的无向无权图,顶点编号为1N,求从顶点1开始,到其他各点的最短路条数

void bfs(){//P1144
...
		for(int v:a[u]){	
			if(d[v]>d[u]+1){
				d[v]=d[u]+1;
				f[v]=f[u];
				q.push(v);
			}else if(d[v]==d[u]+1){
				f[v]=(f[v]+f[u])%p;
			}
		}
...
}

P1462

二分点权,最短路判定

不开longlong见祖宗

P1119

理解Floyd本质

void floyd(){
	init();
	int now=1;
	while(q--){
		int u=read()+1,v=read()+1,tim=read();
		while(t[now]<=tim&&now<=n){
			for(int i=1;i<=n;i++)
				for(int j=1;j<=n;j++)
					f[i][j]=min(f[i][j],f[i][now]+f[now][j]);
			now++;
		}
        ask(u,v);
	}
}

P1772

动态规划

枚举运输路线的更换时间j

f[i]=min(f[j1]+(ij+1)×dijkstra(j,i)+k)),j[1,i]

dijkstra时忽略不可到达的点

最终结果为f[n]k

ll dijkstra(ll tl,ll tr){
	...
		for(int j=0;j<a[now.u].size();j++){
			ll v=a[now.u][j].v,w=a[now.u][j].w;
			if(check(v,tl,tr)){//判断点是否可行
				if(dist[v]>dist[now.u]+w){
					dist[v]=dist[now.u]+w;
					q.push(node{v,dist[v]});
				}
			}
		}
	...
}
for(int i=1;i<=n;i++){
    for(int j=1;j<=i;j++){
        f[i]=min(f[i],f[j-1]+1ll*(i-j+1)*dijkstra(j,i)+k);

P2371

并查集

void init(){//P3367
	for(int i=1;i<=n;i++)f[i]=i;
}
int find(int i){
	if(f[i]==i)return i;
	else return f[i]=find(f[i]);//路径压缩优化
}
void merge(int i,int j){//合并
	f[find(i)]=find(j);
}
bool query(int i,int j){//查询
	return find(i)==find(j);
}

优化

1.路径压缩

2.按秩(启发式)合并

void init(){
    for(int i=1;i<=n;i++)f[i]=i,siz[i]=1;
}
void merge(int i,int j){
	i=find(i),j=find(j);
	if(i==j)return;
	if(siz[i]>siz[j])swap(i,j);
	f[i]=j;siz[j]+=siz[i];
}

任意一种优化单词均摊复杂度为O(logn)

全部使用复杂度为O(α(n))

问题

1.维护每个集合的点权和/maxn

带权并查集

2.统计目前集合数量

for(int i=1;i<=n;i++)if(f[i]==i)ans++;

3.将某元素从集合A移动到集合B

修改根,维护信息

4.利用并查集维护n个命题,有m个条件

条件为pqp¬q两种

开正反两个并查集

注意:必要的时候舍弃“路径压缩”操作。因为它破坏了并查集的树结构。同时会造成一些复杂度错误。

P2024

//正反集
int f[150004];// 同类 敌人 食物 
		
if(x>n||y>n||(x==y&&opt==2)){
    ans++;
    continue;
}
if(opt==1){
    if(find(x+n)==find(y)||find(x+2*n)==find(y)){
        ans++;
    }else{
        merge(x,y);
        merge(x+n,y+n);
        merge(x+2*n,y+2*n);
    }
}else{
    if(find(x)==find(y)||find(x+n)==find(y)||find(y+2*n)==find(x)){
        ans++;
    }else{
        merge(x+2*n,y);
        merge(x+n,y+2*n);
        merge(x,y+n);
    }
}
//带权并查集
int find(int i){
	if(f[i]!=i){
		int tmp=f[i];
		f[i]=find(f[i]);
		d[i]=(d[i]+d[tmp])%3;
		return f[i];
	}else return i;
}

if(w==1)w=0;
if(u==v&&w==2){
    ans++;
}else if(u>n||v>n){
    ans++;
}else{
    int fu=find(u),fv=find(v);
    if(fu!=fv){
        f[fu]=fv;
        d[fu]=(w+d[v]-d[u]+3)%3;
    }else if((d[u]-d[v]+3)%3!=w){
        ans++;
    }
}

生成树

最小生成树:边权和最小的生成树。

瓶颈生成树:最大边权最小的生成树。

最小瓶颈路:两个点之间最大边权最小的简单路径。

性质:

最小生成树是瓶颈生成树的充分不必要条件,即一棵最小生成树一定是一棵瓶颈生成树,一棵瓶颈生成树不一定是一棵最小生成树。

最小生成树上两个点的路径一定是一个最小瓶颈路。

算法

1.Kruskal

O(mlogm)

2.Prim

O(mlogm)

问题

1.思考Kruskal的算法原理,求结点1到结点n的最小瓶颈路的权值

节点1所在集合与节点n所在集合合并时,kruskal所选的边权w即为结点1到结点n的最小瓶颈路的权值

2.并查集与Kruskal的算法关系密切。在不使用路径压缩的情况下,整张图的并查集会呈现一棵树的样貌。这棵树和最小生成树有什么关系

瓶颈路

3.比较Prim和Dijkstra算法的相似性。指出为何Prim可以处理负权边和Dijkstra不行

4.获得 “严格次小生成树”

void dfs(int i,int fa){//P4180
	d[i]=d[fa]+1;
	for(int j=1;(1<<j)<=d[i];j++){
		dp[i][j]=dp[dp[i][j-1]][j-1];
		g[i][j]=max(g[dp[i][j-1]][j-1],g[i][j-1]);
        //维护次大值
		h[i][j]=max(h[dp[i][j-1]][j-1],h[i][j-1]);
		if(g[dp[i][j-1]][j-1]!=g[i][j-1]){
			h[i][j]=max(h[i][j],min(g[dp[i][j-1]][j-1],g[i][j-1]));
		}
	}
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j].v,w=a[i][j].w;
		if(v!=fa){
			dp[v][0]=i;
			g[v][0]=w;
			dfs(v,i);
		}
	}
}
int ask(int i,int j,int lim){
	int ret=0;
	if(d[i]<d[j])swap(i,j);
	for(int k=dis;k>=0;k--){
		if(d[i]-(1<<k)>=d[j]){
			if(g[i][k]==lim)ret=max(ret,h[i][k]);
			else ret=max(ret,g[i][k]);
			i=dp[i][k];
		}
	}
	if(i==j)return ret;
	for(int k=dis;k>=0;k--){
		if(dp[i][k]!=dp[j][k]){
			if(g[i][k]==lim)ret=max(ret,max(h[i][k],h[j][k]));
			else ret=max(ret,max(g[i][k],g[j][k]));
			i=dp[i][k];
			j=dp[j][k];
		}
	}
	if(g[i][0]!=lim)ret=max(ret,g[i][0]);
	if(g[j][0]!=lim)ret=max(ret,g[j][0]);
	return ret;
}
void solve(){
	ans=inf;
	for(int i=1;i<=m;i++){
		if(vis[i]&&e[i].u!=e[i].v){//判断自环
			int tmp=ask(e[i].u,e[i].v,e[i].w);
			ans=min(ans,sum+e[i].w-tmp);
		}
	}
	cout<<ans;
}
init();
kruskal();//先求最小生成树
dfs(1,0);//预处理最大值和次大值
solve();//用未选边替换,更新答案

6.思考最小生成树构建时的贪心性质,简要解释为何最小生成树不止一个

P1967

求最大生成树

在生成树上倍增预处理最小值

lca查询最小值

void dfs(int i,int fa){
	d[i]=d[fa]+1;
	col[i]=nums;
	for(int j=1;(1<<j)<=d[i];j++){
		dp[i][j]=dp[dp[i][j-1]][j-1];
		maxn[i][j]=min(maxn[dp[i][j-1]][j-1],maxn[i][j-1]);
	}
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j].y,w=a[i][j].z;
		if(v!=fa){
			dp[v][0]=i;
			maxn[v][0]=w;
			dfs(v,i);
		}
	}
}
int ask(int i,int j){
	int ret=0x3f3f3f3f;
	if(d[i]<d[j])swap(i,j);
	for(int k=dis;k>=0;k--){
		if(d[i]-(1<<k)>=d[j]){
			ret=min(ret,maxn[i][k]);
			i=dp[i][k];
		}
	}
	if(i==j)return ret;
	for(int k=dis;k>=0;k--){
		if(dp[i][k]!=dp[j][k]){
			ret=min(ret,min(maxn[i][k],maxn[j][k]));
			i=dp[i][k];
			j=dp[j][k];
		}
	}
	return min(ret,min(maxn[i][0],maxn[j][0]));
}
init();//预处理
kruskal();//求生成树
for(int i=1;i<=n;i++){
    if(!d[i]){
        nums++;//图不连通
        dfs(i,0);
    }
}
solve();//查询

CF1513D

树上倍增

求lca

void init(int i,int fa){//P3379
	d[i]=d[fa]+1;
	f[i][0]=fa;
	for(int j=1;(1<<j)<=d[i];j++){
		f[i][j]=f[f[i][j-1]][j-1];
	}
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j];
		if(v!=fa){
			init(v,i);
		}
	}
}
int lca(int i,int j){
	if(d[j]>d[i])swap(i,j);
	for(int k=stp;k>=0;k--){//k>=0注意
		if(d[i]-(1<<k)>=d[j]){
			i=f[i][k];
		}
	}
	if(i==j)return i;
	for(int k=stp;k>=0;k--){//k>=0
		if(f[i][k]!=f[j][k]){
			i=f[i][k];
			j=f[j][k];
		}
	}
	return f[i][0];
}
init(s,0);
lca(i,j);

问题

1.求出 k 个点的树上最近公共祖先

2.将k个点两两求树上最近公共祖先

P4281

暴力去找

pair<int,int> ask(int i,int j){
	int ret=0;
	if(d[i]<d[j])swap(i,j);
	for(int k=dis;k>=0;k--){
		if(d[i]-(1<<k)>=d[j]){
			ret+=dp[i][k];
			i=f[i][k];
		}
	}
	if(i==j)return make_pair(i,ret);
	for(int k=dis;k>=0;k--){
		if(f[i][k]!=f[j][k]){
			ret+=dp[i][k]+dp[j][k];
			i=f[i][k];
			j=f[j][k];
		}
	}
	return make_pair(f[i][0],ret+2);
}
pair<int,int> getp(int A,int B,int C){
	pair<int,int> tmp=ask(B,C);
	pair<int,int> ans=ask(tmp.first,A);
	return make_pair(tmp.first,tmp.second+ans.second);
}
while(m--){
    int x=read(),y=read(),z=read();
    pair<int,int> t1=getp(y,z,x),t2=getp(x,z,y),t3=getp(z,y,x);	
    if(t1.second>t2.second)swap(t1,t2);
    if(t1.second>t3.second)swap(t1,t3);
    cout<<t1.first<<" "<<t1.second<<endl;
}

树上差分

查询

1.求树上路径经过点的个数

=dep[u]+dep[v]dep[fa[lca]]dep[lca]

2.求树上路径的边权和

=dist[u]+dist[v]2×dist[lca]

修改

pq路径上的每个点点权+v

val[p]=val[p]+v

val[q]=val[q]+v

val[lca]=val[lca]v

val[f[lca]]=val[f[lca]]v

dfs自下向上查询

P2680

运输计划为树上一条路径

使得运输时间最长的计划运输时间最短,二分

对于大于mid的运输计划,找到这些运输计划共同经过的最大边

判断共同经过使用树上差分

inline卡常

inline void dfs(int i,int fa){
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j].v,w=a[i][j].w;
		if(v!=fa){
			dfs(v,i);
			val[i]+=val[v];
		}
	}
	for(int j=0;j<a[i].size();j++){//在更新完val[]后再判断
		int v=a[i][j].v,w=a[i][j].w;
		if(v!=fa){
			if(val[i]==cnt&&val[v]==cnt){//该边被cnt条路径经过
				dmax=max(dmax,w);
			}
		}
	}
}
inline bool check(ll x){
	maxn=0;dmax=0;cnt=0;//最长运输计划耗时	可减少的最大时间	需要减少时间的边数
	memset(val,0,sizeof val);//注意
	for(int i=1;i<=m;i++){
		if(q[i].w>x){
			int u=q[i].u,v=q[i].v;
			maxn=max(maxn,q[i].w);cnt++;
			int Lca=getsum(u,v).first;
			//树上差分
            val[u]++;
			val[v]++;
			val[Lca]--;
			val[f[Lca][0]]--;
		}
	}
	dfs(1,0);
	return maxn-dmax<=x;
}
ll l=0,r=maxn;
while(l<r){
    ll mid=l+(r-l)/2;
    if(check(mid))r=mid;
    else l=mid+1;
}

hdu6053

有一棵树,有n个节点,每个节点都有一个用整数表示的颜色类型,其中节点i的颜色是ci

每两个不同节点之间的路径是唯一的,我们定义路径的值为其中出现的不同颜色的数量

计算所有路径的值的总和,该树上总共有n(n1)2条路径

定义[P]P为真则为1,否则为0

即求

i=1n(n1)2j=1colnums[cj>=1]

=j=1colnumsi=1n(n1)2[cj>=1]

=colnums×n(n1)2j=1colnumsi=1n(n1)2[cj=0]

// 以1节点为根
ll fi(ll i){//i为连通块大小,返回连通块边数
	return i*(i-1)/2;
}
void dfs(int i,int fa){
	//sum[i]表示当前以i颜色节点为根的子树大小
    ll inssum=0;//记录增长
	siz[i]=1;//siz[i]表示以节点i为根的子树大小
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j];
		if(v!=fa){
			ll pre=sum[c[i]];//记录过去大小
			dfs(v,i);//递归子树
			siz[i]+=siz[v];//更新
			ll ins=sum[c[i]]-pre;//递归子树之后,sum[c[i]]的增长值,也就是子树内以i颜色节点为根的子树大小
			sub+=fi(siz[v]-ins);//子树大小-ins即为颜色c[i]不出现的连通块大小
			inssum+=ins;//记录
		}
	}
	sum[c[i]]+=siz[i]-inssum;//加上以当前节点为根的子树内的其它节点个数
}
void solve(){
	dfs(1,0);//统计sub,sub为每种颜色未出现边数之和
	for(int i=1;i<=n;i++)
		cnt+=(csum[i]>0);//统计颜色种类
	//统计每种颜色最浅节点到根之间的贡献
    vis[c[1]]=1;
	for(int i=1;i<=n;i++){
		if(!vis[c[i]]){
			sub+=fi(n-sum[c[i]]);
			vis[c[i]]=1;
		}
	}
    ans=cnt*fi(n)-sub;
	++cas;
	printf("Case #%lld: %lld\n",cas,ans);
}
Clear();//注意
for()csum[c[i]]++;//统计每种颜色出现次数
solve();

P4768

P1600

P3953

P1084

posted @   wertyuio1  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示