Loading

基础图论问题总结

最小生成树问题

  • P1265 公路修建

    题目大意:见原题。

    本题关键是看出来这个题是让求最小生成树。如何看出呢?先看条件1,没什么特别的地方,和kruskal差不多,再看条件2,好奇怪的一个条件,竟然要把三角形中的最短边去掉,那还是最小生成树吗?这时候我们画图分析一下,如果离A最近的是B,离B最近的是C,离C最近的是A,则AB>=BC,BC>=AC,AC>=AB,显然必须AB=BC=AC才行。所以即使出现了三角形情况,也只是删去一条和其他边相等的边。综上所述,这道题就是让求一棵最小生成树。由于此题结点有5000个,且是稠密图,所以用kruskal算法一定会超时,所以要用朴素的prim算法(优先队列优化也会超时)。直接建图也会爆空间,所以我们每次现算两点之间距离就好了。本题经验是:图上要求出一棵树的问题,一定要先考虑是不是求最小(大)生成树,甚至是(严格)次小生成树,这些特殊的树容易成为考点;另外就是prim算法每轮是先找出离目前建成的树距离最短的点加入,是树点距,不是点点距。

    代码如下:

    #include <cstdio>
    #include <cstdlib>
    #include <cmath>
    #include <algorithm>
    #define ll long long
    #define INF 9999999999
    using namespace std;
    const int N=5e3+9;
    typedef struct{
    	double x,y;
    }Vertex;
    Vertex v[N];
    ll n,vis[N];
    double dist[N];
    void prim();
    double getdist(Vertex a,Vertex b);
    int main(){
    	scanf("%lld",&n);
    	for(int i=1;i<=n;i++){
    		scanf("%lf %lf",&v[i].x,&v[i].y);
    	}
    	prim();
    	return 0;
    }
    void prim(){
    	double mindist=INF,ans=0;
    	ll minpos=0;
    	for(int i=1;i<=n;i++){
    		dist[i]=INF;
    	}
    	dist[1]=0;
    	for(int i=1;i<=n;i++){
    		mindist=INF;
    		minpos=0;
    		for(int j=1;j<=n;j++){
    			if(mindist>dist[j] && !vis[j]){
    				mindist=dist[j];
    				minpos=j;
    			}
    		}
    		vis[minpos]=1;
    		ans+=mindist;
    //		printf("%d %.2lf\n",i,ans);
    		for(int j=1;j<=n;j++){
    			if(getdist(v[minpos],v[j])<dist[j]){
    				dist[j]=getdist(v[minpos],v[j]);
    			}
    		}
    	}
    	printf("%.2lf\n",ans);
    }
    double getdist(Vertex a,Vertex b){
    	return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
    }
    
  • P3385 负环模板题

    题目大意:判断图中的1号点能否到达图中的负环,如果图中没有负环或者有负环但是没法达到,则也不行。

    负环问题首先可以考虑用Bellman-Ford算法做。这个算法是我刚学的,还不太熟悉,所以说一些细节:我们是通过看第n轮松弛时还有没有可以松弛的边来判断是否有负环的,如果有负环,则第n轮仍然可以松弛。但是这道题还要判断1能否到达这个负环,所以,在每次松弛的时候,只有被松弛的边起点的dist不为INF且满足松弛条件的时候,我们才可以进行松弛,这样能保证我们每次松弛时,涉及到的结点都是1号结点可以到达的。

    代码如下:

    #include <cstdio>
    #include <cstdlib>
    #include <algorithm>
    #define ll long long
    #define INF 2147483647
    using namespace std;
    typedef struct{
    	ll from,to,nxt,weight;
    }Edge;
    const int N=2e3+9;
    const int M=6e3+9;
    Edge edge[M];
    ll cnt=0,head[N],n,m,t,dist[N];
    void add(ll u, ll v, ll w);
    int main(){
    	ll u,v,w,flag;
    	scanf("%lld",&t);
    	while(t--){
    		scanf("%lld %lld",&n,&m);
    		for(int i=0;i<cnt;i++){
    			edge[i].from=0;
    			edge[i].nxt=0;
    			edge[i].to=0;
    			edge[i].weight=0;
    		}
    		cnt=0;
    		flag=0;
    		for(int i=0;i<=n;i++){
    			head[i]=-1;
    			dist[i]=INF;
    		}
    		dist[1]=0;
    		for(int i=1;i<=m;i++){
    			scanf("%lld %lld %lld",&u,&v,&w);
    			add(u,v,w);
    			if(w>=0){
    				add(v,u,w);
    			}
    		}
    		for(int i=1;i<n;i++){
    			for(int j=0;j<cnt;j++){
    				if(dist[edge[j].from]!=INF) //这里对于本题来说非常重要
    					dist[edge[j].to]=min(dist[edge[j].to],dist[edge[j].from]+edge[j].weight);
    			}
    		}
    		for(int j=0;j<cnt;j++){
    			if(dist[edge[j].to]>dist[edge[j].from]+edge[j].weight && dist[edge[j].from]!=INF){
    				flag=1;
    				break;
    			}
    		}
    		if(flag==1){
    			printf("YES\n");
    		} else {
    			printf("NO\n");
    		}
    	}
    	return 0;
    }
    void add(ll u, ll v, ll w){
    	edge[cnt].from=u;
    	edge[cnt].to=v;
    	edge[cnt].weight=w;
    	edge[cnt].nxt=head[u];
    	head[u]=cnt++;
    }
    

    当然,我们也可以用spfa算法来做,毕竟这个算法目前活着的意义大概就是判负环和做负边权的最短路问题了。由于我也是刚学这个算法,所以很多spfa的细节最开始也没有注意到。spfa是用队列来优化的Bellman-Ford算法,队列的作用是每次松弛都是基于前面松弛过的点(根据松弛操作的特性,只有基于这样的点才能得到最短路径)。我们也是通过判断某个点入队了多少次来判断是否有负环的。在从队列中取出一个点,并且基于这个点进行松弛的时候,如果图中有平行边,那么很有可能会松弛成功两次,但是它都是同一轮松弛,所以我们只能入队一次,所以每一轮我们至多让入队标记+1,待会儿可以在代码的那行注释里再去理解。最后如果检测到松弛了n次的点时,我们就可以判断有负环了。由于第一个入队的元素是1号结点,所以负环肯定是1号结点能到达的。

    代码如下:

    #include <cstdio>
    #include <cstdlib>
    #include <queue>
    #include <algorithm>
    #define ll long long
    #define INF 9999999999
    using namespace std;
    typedef struct{
    	ll from,to,nxt,weight;
    }Edge;
    const int N=2e3+9;
    const int M=6e3+9;
    Edge edge[M];
    ll n,m,t,head[N],cnt,sign[N],dist[N],num[N];
    queue<ll> q;
    void add(ll u,ll v,ll w);
    int main(){
    	ll u,v,w,tmp,flag;
    	scanf("%lld",&t);
    	while(t--){
    		scanf("%lld %lld",&n,&m);
    		cnt=0;
    		for(int i=0;i<=n;i++){
    			head[i]=-1;
    			sign[i]=0;
    			dist[i]=INF;
    			num[i]=0;
    		}
    		for(int i=0;i<m;i++){
    			scanf("%lld %lld %lld",&u,&v,&w);
    			add(u,v,w);
    			if(w>=0){
    				add(v,u,w);
    			}
    		}
    		while(!q.empty()){
    			q.pop();
    		}
    		sign[1]=1;
    		dist[1]=0;
    		num[1]=1;
    		flag=0;
    		q.push(1); //起点先入队
    		while(!q.empty()){
    			tmp=q.front();
    			q.pop();
    			sign[tmp]=0; //出队时要把标记去掉
    			for(ll i=head[tmp];i>=0;i=edge[i].nxt){
    				if(dist[edge[i].to]>dist[edge[i].from]+edge[i].weight){
    					dist[edge[i].to]=dist[edge[i].from]+edge[i].weight; //松弛是必须的
    					if(sign[edge[i].to]==0){ //但是每一轮只能入队一次
    						sign[edge[i].to]=1;
    						num[edge[i].to]++; //统计曾经的入队次数
    						if(num[edge[i].to]>=n){
    							flag=1;
    							break;
    						}
    						q.push(edge[i].to);
    					}
    				}
    			}
    			if(flag==1){
    				break;
    			}
    		}
    		if(flag){
    			printf("YES\n");
    		} else {
    			printf("NO\n");
    		}
    	}
    	return 0;
    }
    void add(ll u,ll v,ll w){
    	edge[cnt].from=u;
    	edge[cnt].to=v;
    	edge[cnt].weight=w;
    	edge[cnt].nxt=head[u];
    	head[u]=cnt++;
    }
    
  • P1967 货车运输

    题目大意:有一个无向图,q次询问,每次询问给出两个点,这两个点之间可能有多条路径,求一条最小边权最大的路径,并输出最小边权。

    上面说过了,这种问题提到了“最小边权最大”这一要求,我们可能会想到最大瓶颈生成树,要求最大瓶颈生成树,我们可以考虑求最大生成树,但这对么?我先来证明一下正确性:

    假设我们要求的那棵树不是最大生成树,那么,最大生成树中必然存在两个点A,B,其路径上的最小边权本可以更大,且其他任意两点之间的路径中最小边的边权不会更小。我们可以在最大生成树上断开这条边,则树变成了两棵树tree1和tree2。由刚才的假设,图中必定存在一条能连接这两棵树的边(想象成桥梁),使得构成的新树中,AB之间的边权最小的边更大,且其他任意两点之间的路径上的最小边权的最大值不小于在最大生成树中的。由于新树与原树仅相差一条边,所以,整棵树边权和的变化只需考虑删去的边和新增的边。由于新树中,任意两点之间的路径上的最小边权不比原来的小,且存在两点之间的最小边权比原来严格大,所以,整棵树的边权和必然变大了,也就是说新增的边要比删去的边边权更大,即我们得到了一棵更大生成树,矛盾。所以,假设不成立,我们要求的树就是最大生成树,证毕。

    求出了最大生成树,接下来就是维护两点之间路径的边权的最值了。显然点权最值可以用树剖,昨天又学会了边权转点权,那么树剖肯定能做。不过用树剖会使人变傻,并且树剖也不快,我们尝试一下能不能想出来别的方法。其实可以直接用lca。每次求\(fa[now][i]\)的时候,都进行一次\(minw[now][i]=min(minw[now][i-1],minw[fa[now][i-1]][i-1])\),即\(now\)到其\(2^i\)级父亲的边权最小值为\(now\)到其\(2^{i-1}\)级父亲的边权的最小值与\(now\)\(2^{i-1}\)级父亲的\(2^{i-1}\)级父亲的最小值中的较小值。

    注意到这个题的图都不一定连通,有点难受,那么如果用kruskal求MST,在不连通的图里面求,会求出来什么呢?应该是求出多个连通分量。这样的话,在dfs预处理各种祖先的时候,可能要不止一次进行dfs,这个在DS课上就学过了,用一个循环就好了。对于查询,我们需要在不断调整x与y的深度的时候更新最小值。当然,我们先得用并查集看看是否可达,不可达直接返回-1就好了。积累经验:无向图两个点是否互相可达可以用并查集

    代码实现中有一个注意点,就是在往上跳的过程中,刷新ans时x和y都要参与,最后一步也是都要参与。

    代码基本就是lca+mst,稍作修改了一下,注意上面说的细节就好了。

    代码如下:

    #include <cstdio>
    #include <cstdlib>
    #include <algorithm>
    #define ll long long 
    #define INF 9999999999
    using namespace std;
    const int N=1e4+9;
    const int M=1e5+9;
    ll n,m,q,cnt,head[N],f[N],num,sign[N],depth[N],lg[N];
    ll fa[N][20],minw[N][20];
    typedef struct{
    	ll from,to,nxt,weight;
    }Edge;
    Edge edge[M];
    Edge mst[2*N];
    ll find(ll x);
    void un(ll x,ll y);
    ll lca(ll x,ll y);
    void kruskal();
    bool together(ll x,ll y);
    void add(ll x,ll y,ll z);
    void dfs(ll now,ll father,ll d,ll w);
    int main(){
    	ll x,y,z;
    	scanf("%lld %lld",&n,&m);
    	for(int i=0;i<=n;i++){
    		head[i]=-1;
    		f[i]=i;
    	}
    	while(m--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y,z);
    		add(y,x,z);
    	}
    	kruskal();
    	cnt=0;
    	for(int i=0;i<=n;i++){
    		head[i]=-1;
    	}
    	for(int i=0;i<num;i++){
    		add(mst[i].from,mst[i].to,mst[i].weight);
    		add(mst[i].to,mst[i].from,mst[i].weight);
    	}
    	for(ll i=2;i<=n;i++){
    		lg[i]=lg[i/2]+1;
    	}
    	for(ll i=1;i<=n;i++){
    		if(!sign[i]){
    			dfs(i,0,1,0);
    		}
    	}
    	scanf("%lld",&q);
    	while(q--){
    		scanf("%lld %lld",&x,&y);
    		printf("%lld\n",lca(x,y));
    	}
    	return 0;
    }
    ll lca(ll x,ll y){
    	ll ans=INF;
    	if(!together(x,y)){
    		return -1;
    	}
    	if(x==y){
    		return 0;
    	}
    	if(depth[x]<depth[y]){
    		swap(x,y);
    	}
    	while(depth[x]>depth[y]){
    		ans=min(ans,minw[x][lg[depth[x]-depth[y]]]);
    		x=fa[x][lg[depth[x]-depth[y]]];
    	}
    	if(x==y){
    		return ans;
    	}
    	for(ll i=lg[depth[x]];i>=0;i--){
    		if(fa[x][i]!=fa[y][i]){
    			ans=min(ans,minw[x][i]);
    			ans=min(ans,minw[y][i]);
    			x=fa[x][i];
    			y=fa[y][i];
    		}
    	}
    	ans=min(ans,minw[x][0]); //重要
    	ans=min(ans,minw[y][0]);
    	return ans;
    }
    void dfs(ll now,ll father,ll d,ll w){
    	sign[now]=1;
    	depth[now]=d;
    	fa[now][0]=father;
    	minw[now][0]=w; //node 1's ?
    	for(ll i=1;i<=lg[d];i++){
    		minw[now][i]=min(minw[now][i-1],minw[fa[now][i-1]][i-1]);
    		fa[now][i]=fa[fa[now][i-1]][i-1];
    	}
    	for(ll i=head[now];i>=0;i=edge[i].nxt){
    		if(edge[i].to!=father){
    			dfs(edge[i].to,now,d+1,edge[i].weight);
    		}
    	}
    }
    bool together(ll x,ll y){
    	return find(x)==find(y);
    }
    ll find(ll x){
    	if(x==f[x]){
    		return f[x];
    	}
    	return f[x]=find(f[x]);
    }
    void un(ll x,ll y){
    	ll fx,fy;
    	fx=find(x);
    	fy=find(y);
    	f[fx]=f[fy];
    }
    void kruskal(){
    	sort(edge,edge+cnt);
    	for(ll i=0;i<cnt;i++){
    		if(!together(edge[i].from,edge[i].to)){
    			un(edge[i].from,edge[i].to);
    			mst[num]=edge[i];
    			num++;
    		}
    	}
    }
    bool operator <(const Edge &p,const Edge &q){
    	return p.weight>q.weight;
    }
    void add(ll x,ll y,ll z){
    	edge[cnt].from=x;
    	edge[cnt].to=y;
    	edge[cnt].nxt=head[x];
    	edge[cnt].weight=z;
    	head[x]=cnt++;
    }
    
  • P4180 严格次小生成树

    题目大意:显然

    我们现在已经会求最小生成树了,那么,我们可以考虑在最小生成树的基础上进行改造,然后构造出严格次小生成树。由最小生成树的构造过程可以知道,如果断开MST中的一条边,然后再用另一条之前不在MST中的边去连接刚才的两部分,那么得到的树的权值之和肯定不小于MST的权值。那么如果断开两条边然后重连两条边呢?显然断开两条边,使得MST被分成了三个连通分支,然后我们又在这三个连通分支之间架了两座新的桥从而生成了一棵新的树。显然,新的两条边的权值都不可能小于原来两条边的权值,否则,就能生成更小生成树,矛盾。所以改两条边比改一条边的权值和变化得要更多。所以经过一波推理,我们发现,每次改动MST的一条边,在数据有保证的情况下,能生成严格次小生成树。

    所以我的思路是:先把MST建出来,记录选了哪些边,然后遍历图中所有未选过的边,每次将一条之前未选过的边加入MST中,产生且仅产生一个环,然后检测环中的最大边,在MST的基础上减去它的权值,加上新边的权值,得到一个结果。注意是最大边,因为每插入一条新边,便产生一个环,由MST的建立过程,知新插入的那条边肯定不小于环中剩下的其他边,所以替换最大的边才能让变化比较小。如果发现最大边和新插入的边大小一样,则需要替换严格次大边,才能保证严格次小。

    显然,插入新边时,我们是知道MST上的哪条路径和新边组成环的,问题在于如何快速查询到MST上这条路径上的边权的最大值和次大值,有了上面那道题的基础,我们知道可以倍增来实现快速查询。注意,一旦出现要查询树上的某些信息的时候,一定要考虑效率,不要上来就想暴力,倍增和树剖不香吗?

    关于次大值的维护:显然,从结点\(now\)到它的\(2^i\)级父亲的路径上的次大值,肯定是\(now\)到它的\(2^{i-1}\)级父亲的路径上的最大和次大,以及\(now\)\(2^{i-1}\)级父亲到其\(2^{i-1}\)级父亲上的最大和次大这四个中的一个,看情况取。

    关于代码,我想说,这题太玄了。可能是我造的数据有问题,对拍一直没法无差异,但是提交AC:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 9999999999999999
    using namespace std;
    const int M=6e5+9;
    const int N=1e5+9;
    typedef struct{
    	ll from,to,nxt,weight;
    }Edge;
    Edge edge[M],mst[2*N];
    ll cnt,head[N],fa[N][20],maxx[N][20],ndmaxx[N][20],n,m,depth[N],mintree,num,f[N],choose[N],mstcnt,msthead[N],lg[N],ndmintree=INF;
    ll sign[M];
    void add(ll x,ll y,ll z);
    bool together(ll x,ll y);
    void un(ll x,ll y);
    ll find(ll x);
    void kruskal();
    ll lcamaxx(ll x,ll y);
    ll lcandmaxx(ll x,ll y);
    void create_tree();
    void mstadd(ll x,ll y,ll z);
    void dfs(ll now,ll father,ll w,ll d);
    int main(){
    	ll x,y,z;
    	scanf("%lld %lld",&n,&m);
    	for(int i=0;i<=n;i++){
    		head[i]=-1;
    		f[i]=i;
    	}
    	while(m--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y,z);
    //		add(y,x,z); 巨懵逼,如果存双向边,会在判断是否选过某条边上出问题!!!!
    	}
    	kruskal();
    	create_tree();
    	dfs(1,0,0,1);
    	for(int i=0;i<cnt;i++){
    		if(sign[i]) {
    			continue; //如果是之前选过的边,则跳过 
    		} 
    		else{
    			ll maxedge=lcamaxx(edge[i].from,edge[i].to); //先看一下最大值行不行
    			if(maxedge==edge[i].weight){
    				ll ndmaxedge=lcandmaxx(edge[i].from,edge[i].to);
    				if(ndmaxedge==edge[i].weight){
    					continue;
    				} else {
    					ndmintree=min(ndmintree,mintree-ndmaxedge+edge[i].weight);
    				}
    			} else {
    				ndmintree=min(ndmintree,mintree-maxedge+edge[i].weight);
    			}
    		}
    	}
    	printf("%lld\n",ndmintree);
    //	for(ll i=1;i<=n;i++){
    //		printf("the second largest between %lld and its father is %lld\n",i,ndmaxx[i][0]);
    //	}
    //	for(ll i=1;i<=n;i++){
    //		for(ll j=i;j<=n;j++){
    //			printf("path between %lld and %lld's second largest is %lld\n",i,j,lcandmaxx(i,j));
    ////			printf("%lld's 2^%lld father is%lld\n",i,j,fa[i][j]);
    //		}
    //	}
    	return 0;
    } 
    inline ll lcamaxx(ll x,ll y){
    	ll ans=0; //由于是在树上查询的,所以肯定没有自环 
    	if(depth[x]<depth[y]){
    		swap(x,y);
    	}
    	while(depth[x]>depth[y]){
    		ans=max(ans,maxx[x][lg[depth[x]-depth[y]]]);
    		x=fa[x][lg[depth[x]-depth[y]]];
    	}
    	if(x==y){
    		return ans;
    	}
    	for(ll i=lg[depth[x]];i>=0;i--){
    		if(fa[x][i]!=fa[y][i]){
    			ans=max(ans,maxx[x][i]);
    			ans=max(ans,maxx[y][i]);
    			x=fa[x][i];
    			y=fa[y][i];
    		}
    	}
    	ans=max(ans,maxx[x][0]);
    	ans=max(ans,maxx[y][0]);
    	return ans;
    }
    inline ll lcandmaxx(ll x,ll y){
    	ll ans=0;
    	if(depth[x]<depth[y]){
    		swap(x,y);
    	}
    	while(depth[x]>depth[y]){
    		ans=max(ans,ndmaxx[x][lg[depth[x]-depth[y]]]);
    		x=fa[x][lg[depth[x]-depth[y]]];
    	}
    	if(x==y){
    		return ans;
    	}
    	for(ll i=lg[depth[x]];i>=0;i--){
    		if(fa[x][i]!=fa[y][i]){
    			ans=max(ans,ndmaxx[x][i]);
    			ans=max(ans,ndmaxx[y][i]);
    			x=fa[x][i];
    			y=fa[y][i];
    		}
    	}
    	ans=max(ans,ndmaxx[x][0]);
    	ans=max(ans,ndmaxx[y][0]);
    	return ans;
    }
    void dfs(ll now,ll father,ll w,ll d){ //次大值有问题 
    	depth[now]=d;
    	fa[now][0]=father;
    	maxx[now][0]=w;
    	for(ll i=1;i<=lg[d];i++){
    		fa[now][i]=fa[fa[now][i-1]][i-1];
    		maxx[now][i]=max(maxx[fa[now][i-1]][i-1],maxx[now][i-1]);
    		
    		if(maxx[fa[now][i-1]][i-1]==maxx[now][i-1]){ //如果两个区间最大相等 
    //			printf("turn to branch 1, ");
    			ndmaxx[now][i]=max(ndmaxx[now][i-1],ndmaxx[fa[now][i-1]][i-1]); //则两个区间次大中较大的那个是次大 
    		} else {
    //			printf("turn to branch 2, ");
    			ndmaxx[now][i]=min(maxx[fa[now][i-1]][i-1],maxx[now][i-1]); //先取两个区间最大值中的较小的
    			ndmaxx[now][i]=max(max(ndmaxx[now][i-1],ndmaxx[fa[now][i-1]][i-1]),ndmaxx[now][i]); //再把它和两个区间次大比,选大的 
    		}
    //		printf("the second largest of the path between %lld and its 2^%lld's father is %lld\n",now,i,ndmaxx[now][i]);
    	}
    	for(ll i=msthead[now];i>=0;i=mst[i].nxt){
    		if(mst[i].to!=father){
    			dfs(mst[i].to,now,mst[i].weight,d+1);
    		}
    	}
    }
    inline void create_tree(){
    	for(int i=0;i<=n;i++){
    		msthead[i]=-1;
    	}
    	for(int i=0;i<num;i++){
    		mstadd(edge[choose[i]].from,edge[choose[i]].to,edge[choose[i]].weight);
    		mstadd(edge[choose[i]].to,edge[choose[i]].from,edge[choose[i]].weight);
    	}
    	for(int i=2;i<=n;i++){
    		lg[i]=lg[i/2]+1;
    	}
    }
    inline void mstadd(ll x,ll y,ll z){
    	mst[mstcnt].from=x;
    	mst[mstcnt].to=y;
    	mst[mstcnt].nxt=msthead[x];
    	mst[mstcnt].weight=z;
    	msthead[x]=mstcnt++;
    }
    ll find(ll x){
    	if(x==f[x]){
    		return f[x];
    	}
    	return f[x]=find(f[x]);
    }
    bool together(ll x,ll y){
    	return find(x)==find(y);
    }
    void un(ll x,ll y){
    	f[find(x)]=find(y);
    }
    bool operator <(const Edge &p,const Edge &q){
    	return p.weight<q.weight;
    }
    inline void kruskal(){
    	sort(edge,edge+cnt);
    	for(ll i=0;i<cnt;i++){
    		if(!together(edge[i].from,edge[i].to)){
    			un(edge[i].from,edge[i].to);
    			choose[num++]=i;
    			mintree+=edge[i].weight;
    			sign[i]=1;
    		}
    	}
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].from=x;
    	edge[cnt].to=y;
    	edge[cnt].nxt=head[x];
    	edge[cnt].weight=z;
    	head[x]=cnt++;
    }
    
  • 草莓

    题目大意:见原题。

    这道题的暴力解法是:枚举点集,然后计算这个点集所能得到的最短路的最大值,然而由于这个能重复经过某些点,且走的路有一定的联系,所以不容易算出来这个最大值。

    然后我又想了一下,想在邻接矩阵上观察一些东西,但是也没看出来什么。

    看了题解之后,我才发现题目的更深层次的含义。我们考虑把图连接成一个完全图,则每两个点之间的边的边权就是这两个点在原来那个树上的路径距离了。这个时候,我们想取出一些点,让他们连边,其中最短边最长,则这就是求最大瓶颈生成树的问题,我们可以直接求最大生成树解决。不过这个图是个完全图,并且点很多,有\(10^5\)个,所以prim和kruskal都不可做。

    这个时候,题解又厉害了,它考虑了最大生成树的特点:如果对一个图\(G\)求最大生成树,且已知原树\(T\)的一条直径叫做\((x,y)\),那么这个最大生成树的形式是:\((x,y)\)连边,其他点和\(x,y\)中离得较远的点直接连边。这个为什么是对的呢?我们来简单说一下。对于原树\(T\),我们求出了其直径\(path(x,y)\),现在我们把这棵树补成一个完全图,然后想求出来这个完全图的最大生成树。首先我们连接了\((x,y)\),这是无可非议的,因为\((x,y)\)是完全图中最长的边,它肯定就在最大生成树中了;然后我们考虑其他的点和\(x,y\)的距离,选择较大的一个,直接连边。如果某个点\(a\)不这样不直接连边呢?那它肯定会通过一系列中间点后与\(x,y\)中的某个连起来了,这条路径记为\(p_1\),显然这条路径应该比直接连接权值更大(否则直接连接肯定更优了)。我们考虑\(p_1\)上直接和\(x\)\(y\)相连的那个点\(c\)

最短路问题

  • P1119 灾后重建

    题目大意:见原题。

    这道题目的本质是floyd算法。如果在t天时看A和B点之间的最短路径,那么如果采用松弛操作的话,就只能选择那些在第t天之前就已经通车的点作为中间点。注意到题目有一个很好的输入:村庄1-n的修好的时间是单调不减的,所以对于floyd比较方便。由于只有询问没有修改,所以我考虑先预处理。先考虑能不能预处理出t为值域内所有可能值的情况下的全源最短路径呢?应该是可以的。我们从前往后枚举点,看这个点修好的时间,只处理这离散的n个时间即可,询问的时候只要找到它属于那两个点的时间之间就好了。这样的话,要做n次floyd,每次floyd时间复杂度是\(O(k^3)\),空间复杂度是\(O(n^2)\) ,k表示的是在时间为t的时候,有k个点修好了。这样总的时间复杂度应该是\(O(n^4)\),空间复杂度是\(O(n^3)\) ,显然是过不了的。

    不开O2只能拿40分,但是我惊讶地发现开了O2会AC,并且会发现,优化极为明显( 某个点770ms->89ms)。可能是因为floyd算法本身紧凑的特点导致开O2很快吧。

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 99999999999
    using namespace std;
    const int N=2e2+3;
    ll graph[N][N];
    ll ans[N][N][N];
    ll n,m,q;
    ll t[N];
    void pre();
    int main(){
    	ll x,y,z,pos;
    	scanf("%lld %lld",&n,&m);
    	for(int i=0;i<n;i++){
    		scanf("%lld",&t[i]);
    	}
    	for(int i=0;i<N;i++){
    		for(int j=0;j<N;j++){
    			graph[i][j]=INF;
    		}
    	}
    	for(int i=0;i<N;i++){
    		graph[i][i]=0;
    	}
    	for(int i=0;i<m;i++){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		graph[x][y]=z;
    		graph[y][x]=z;
    	}
    	pre();
    	scanf("%lld",&q);
    	while(q--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		pos=upper_bound(t,t+n,z)-t; //看看z在t数组中的位置 
    		//没有比z大的,即全都修好了 
    		if(pos==n){
    			pos--;
    			if(ans[pos][x][y]==INF){
    				printf("-1\n");
    			} else {
    				printf("%lld\n",ans[pos][x][y]);
    			}
    		} 
    		//全都比z大,则都没修好 
    		else if(pos==0){
    			printf("-1\n");
    		} else {
    			pos--;
    			if(x>pos || y>pos){ //有没修好的城市 
    				printf("-1\n");
    			}
    			else if(ans[pos][x][y]==INF){
    				printf("-1\n");
    			} else {
    				printf("%lld\n",ans[pos][x][y]);
    			}
    		}
    	}
    	return 0;
    } 
    void pre(){
    	for(int i=0;i<n;i++){
    		//求前i个村庄修好时的最短路
    		for(int j=0;j<=i;j++){
    			for(int k=0;k<=i;k++){
    				ans[i][j][k]=graph[j][k];
    			}
    		}
    		for(int j=0;j<=i;j++){
    			for(int k=0;k<=i;k++){
    				for(int l=0;l<=i;l++){
    					ans[i][k][l]=min(ans[i][k][l],ans[i][k][j]+ans[i][j][l]);
    				}
    			}
    		}
    	}
    }
    
  • P1772 物流运输

    题目大意:一张无向加权图,其中有些点有时候不能走,切换一次路径需要消耗能量,走路要消耗能量,问走很多次之后最小消耗为多少。

    考虑到切换有额外花费,所以不一定每次选择最短路就能得到最优解,所以不能只处理n次单源最短路。看到数据范围说只要求100次,并且图只有20个点,所以可能考虑用搜索的方式去求最优解。这个搜索一共有两个dfs(findway和nextway),不出意外肯定会超时。考虑优化搜索,注意到点数\(\le20\) ,所以从1到m至多有\(1+18+18*17+18*17*16+……+18*17*……*2*1\)种走法(20个点的完全图的情况),而仅\(18!=6402373705728000\) 情况太多了,预处理根本存不下。

    由于是求最值,并且涉及决策,所以不难想到可能用到dp。设\(f[i]\)为前i次运输的最小的消耗,如果这样定义,那么,我们要根据下一次的码头的可用的情况和这一次以及下一次决策来推转移方程,但是一维的状态没法表示前一次是怎么决策的。思考如何通过加一维状态来表示出上一次是选择的哪一种方式,如果能预处理所有的1到m的路径,状压表示每条路径走过的点,那就可以用唯一的id来表示每种航线了,那么可以定义\(f[i][j]\)表示前i次运输,并且第i次运输采取的是id为j的航线方案,所能消耗的最小能量。这样的话,可以尝试转移到\(f[i+1][d]\),其中d表示第\(i+1\)次能走的那些航线的\(id\),那么转移方程为:\(f[i+1][d]=min\{f[i][d],min_{j\ne d } \{f[i][j]\}+k\}\) ,其中\(j\)在第\(i\)次可走且\(j \ne d\) 。这个转移方程看起来还是蛮有道理的,时间复杂度是\(O(nm*pathnum)\)。如果这个转移是对的,如果\(pathnum\)比较小,那么这个方法将绝杀,可惜因为可能完全图的极端数据所以处理不得,且转移的正确性我也拿不准。

    又观察了一下题目,看了一下最后的消费的计算式,发现可能有这样一种定义状态的方法:设\(f[i][j]\)表示前\(i\)次运输,改道不超过\(j\)次的最小消费。但为了统计改道次数,还是要看之前走的是哪条路,那样还是走不通。

    突然发现,之前太傻逼了,\(pathnum\) 哪有那么大?明明只有20个点,对于一条路径来说,每个点只有选与不选两种情况,并且这个问题还给定了起点和终点,那么至多有\(2^{18}\)种情况,完全压得住。虽然转移方程还不知道对不对,不过可以先试一下,错了再说。

    算错空间了,所以MLE了,并且还有很多点TLE。目前只知道过了样例。

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 999999999999
    using namespace std;
    const int N=1e2+2;
    const int M=22;
    const int S=(1<<18)+9;
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    typedef struct{
    	bool s;
    	ll len;
    }State; 
    Edge edge[M*(M-1)];
    ll n,m,k,e,head[M],cnt,path[M],sign[M],ans=INF;
    ll unable[N][M],f[N][S],length[N][S]; //每天有哪些点不能用,dp数组,每天可用的状态预处理存储 
    int ok[N][S];
    State state[S]; //最短路id数组 
    void dp();
    void add(ll x,ll y,ll z);
    void dfs(ll now,ll step,ll w);
    ll available(ll tmp1,ll tmp2);
    int main(){
    	ll x,y,z,d,a,b,p;
    	scanf("%lld %lld %lld %lld",&n,&m,&k,&e);
    	for(int i=0;i<=m;i++){
    		head[i]=-1;
    	}
    	for(int i=0;i<e;i++){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y,z);
    		add(y,x,z);
    	}
    	dfs(1,0,0);
    	scanf("%lld",&d);
    	while(d--){
    		scanf("%lld %lld %lld",&p,&a,&b);
    		for(int i=a;i<=b;i++){
    			unable[i][0]++; //第i天不能用的港口+1 
    			unable[i][unable[i][0]]=p; //第p个港口在a-b天不能用 
    		}
    	} 
    	dp();
    	printf("%lld\n",ans);
    	return 0;
    } 
    ll available(ll tmp1,ll tmp2){
    	ll flag=1;
    	while(tmp1){
    		if((tmp2&1)==0 && (tmp1&1)==1){ //如果某个点不可用,但是tmp1选了 
    			flag=0;
    			break;
    		}
    		tmp1=tmp1>>1;
    		tmp2=tmp2>>1;
    	}
    	return flag;
    }
    void dp(){
    	ll ret;
    	for(ll i=0;i<N;i++){
    		for(ll j=0;j<S;j++){
    			f[i][j]=INF; //初始化dp数组,最开始的时候都设置为消耗无穷大 
    		}
    	}
    	for(ll i=1;i<=n;i++){ //预处理第i轮可以用的路径 
    		ret=0; //把不能用的点状态压缩成ret 
    		for(ll j=1;j<=unable[i][0];j++){ //第i轮有unable[i][0]个不能走的点 
    			ret=ret+(1<<(unable[i][j]-1));
    		}
    		ret=~ret; //取反之后ret二进制就是第i轮能走的点
    		for(ll j=0;j<S;j++){ //枚举所有路径 
    			if(!state[j].s) continue; //如果不存在这条路径 
    			if(available(j,ret)){ //如果路径j在第i次走是合法的
    				ok[i][0]++; //第i次可以用的路径+1 
    				length[i][0]++;
    				ok[i][ok[i][0]]=j; //存下来可以用的这条路径 
    				length[i][length[i][0]]=state[j].len; //存下来这条路径的花费 
    			}
    		}
    	}
    	for(ll j=1;j<=ok[1][0];j++){ //把第一轮可以走的初始化一下 
    		f[1][ok[1][j]]=length[1][j]; //ok[1][j]指的是第一轮里面第j条可以走的路径的状压之后的结果 
    	}
    	for(ll i=2;i<=n;i++){
    		for(ll j=1;j<=ok[i][0];j++){ //枚举第i轮可以走的路 
    			for(ll l=1;l<=ok[i-1][0];l++){ //枚举第i-1轮可以走的路 
    				f[i][ok[i][j]]=min(f[i][ok[i][j]],f[i-1][ok[i-1][l]]+length[i][j]+(ok[i][j]==ok[i-1][l]?0:k)); 
    			}
    		}
    	}
    	for(ll i=0;i<S;i++){
    		ans=min(ans,f[n][i]);
    	}
    }
    void dfs(ll now,ll step,ll w){
    	if(now==m){
    		ll ret=0;
    		for(ll i=1;i<step;i++){ //掐头去尾状态压缩 
    			ret=ret+(1<<(path[i]-1));
    		}
    		state[ret].s=true; //标记这条路可以从1到m
    		state[ret].len=w; //记录这条路的消耗 
    		return; 
    	} else {
    		sign[now]=1;
    		path[step]=now;
    		for(ll i=head[now];i>=0;i=edge[i].nxt){
    			if(!sign[edge[i].to]){
    				dfs(edge[i].to,step+1,w+edge[i].weight); 
    			}
    		}
    		sign[now]=0;
    	}
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].nxt=head[x];
    	edge[cnt].weight=z;
    	head[x]=cnt++;
    }
    

    可以滚动数组优化上面的写法的空间复杂度,但是时间上可能没法更优了。

    只好看题解了(至少想过了并且得到了一个不是很优但也不暴力的做法)

    题解中一种思路是这样的:设\(f[i]\)表示前i次运输消耗的最小值,然后用一种分段的思想:虽然切换路径有开销导致每一段不一定选择最短路最合适,但是,可以确定的是,最后一段肯定是要选择最短路的,因为它是最后一段,不需要再切换成别的路了。所以,考虑枚举一个j,转移方程是\(f[i]=min\{f[i],f[j-1]+(i-j+1)*minlen+k\}\) ,这个转移方程中\(min\)的后一项的意思是:前\(j-1\)天的运输最小值\(f[j-1]\)加上第\(j\)到第\(i\)天全部选择长度为\(minlen\)的最短路(与第\(j-1\)天不同)再加上一次切换开销\(k\) ,枚举所有的\(j\),能算出来一系列的值,取其中的最小值,就是\(f[i]\)了。这样就巧妙地利用了最短路径。初始化的话,\(f[1]\)可算,后面的都初始化成无穷大;转移的话,突然发现一个问题:如何保证第\(j\)次和第\(j-1\)次选的路径是不同的呢?似乎我们还是要存储一下之前选的是哪条路,那样岂不是又要状压了?空间不还是会爆吗?先考虑其他问题,假设我们保证了\(j-1\)次和\(j\)次选择不同的路,那么我们只需要在枚举\(j\) 的时候求有限制点的最短路就好了,由于图很小,所以完全可以每次单独求,用裸的dijkstra就可以。下面回到刚才的疑问:如何保证第\(j-1\)天和第\(j\)天选的路径是不一样的?在看题解作者的代码之前,我也是很懵逼的,在看了之后,豁然开朗了。作者在dp时初始化\(f[0]=-k\),虽然\(f[0]\)看似是个根本没用的东西,但是作者还是用它来做一些了不起的事情。不妨假设为了保证\(f[3]\)最小,前3天选的路需要是一样的,我们把它叫做\(path1\),那么算\(f[2]\) 时,由于前两次的限制条件和前三次的不一定一样,所以或许\(f[2]\) 时不会选\(path1\) (当然也可能会选),我们把\(f[2]\) 时选的那条路叫\(path2\)。如果\(path1\ne path2\),那么不难看出来转移方程是没有问题的。那如果 \(path1=path2\) 呢?由于前3天都要选\(path2\),所以转移时如果按照转移方程,最终就是 \(f[3]=min\{f[3],f[0]+3*minlen+k,f[1]+2*minlen'+k,f[2]+minlen''+k\}\) ,由于最优解是三天都是\(path1\),没切换线路,所以只有当\(f[0]=-k\) 的时候,我们才能保证转移方程的正确性。所以,不需要再记录上次选了哪条路。

    这样分析下来,本题的代码就只剩下几行dp和一个带限制的最短路了。

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 99999999999
    using namespace std;
    const int N=1e2+9;
    const int M=22; 
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    Edge edge[M*(M-1)];
    ll dist[M],f[N],cnt,head[M],n,m,k,e,unable[N][M],sign[M],vis[M];
    ll dijkstra();
    void add(ll x,ll y,ll z);
    int main(){
    	ll x,y,z,d,p,a,b;
    	scanf("%lld %lld %lld %lld",&n,&m,&k,&e);
    	for(int i=0;i<=m;i++){
    		head[i]=-1; //最开始忘了初始化了
    	} 
    	while(e--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y,z);
    		add(y,x,z);
    	}
    	scanf("%lld",&d);
    	while(d--){
    		scanf("%lld %lld %lld",&p,&a,&b);
    		for(int i=a;i<=b;i++){
    			unable[i][0]++; //第i天不能用的港口+1 
    			unable[i][unable[i][0]]=p; //第p个港口在a-b天不能用 
    		}
    	}
    	f[0]=-k; //保证j-1和j选的路不同,即使相同也不会出错 
    	for(int i=1;i<=n;i++){
    		f[i]=INF; //忘了初始化了
    	}
    	for(int i=1;i<=n;i++){
    		for(int j=0;j<=m;j++){ //把不能走的点先设为空 
    			sign[j]=0;
    		}
    		for(int j=i;j>=1;j--){ //不断增加不能走的点,直到根本没法走 
    			for(int l=1;l<=unable[j][0];l++){
    				sign[unable[j][l]]=1; //标记为不可走 
    			}
    			ll len=dijkstra();
    			if(len==INF) { //路不通就不能走了 
    				break;
    			}
    			f[i]=min(f[i],f[j-1]+len*(i-j+1)+k);
    		}
    	}
    	printf("%lld\n",f[n]);
    	return 0;
    }
    ll dijkstra(){
    	for(int i=0;i<M;i++){
    		dist[i]=INF;
    		vis[i]=0;
    	}
    	dist[1]=0;
    	ll pos=0,minlen=INF;
    	for(int i=0;i<m;i++){
    		minlen=INF;
    		pos=0;
    		for(int j=1;j<=m;j++){
    			if(!vis[j] && !sign[j] && dist[j]<minlen){ //没找到最短路且可以走 
    				minlen=dist[j];
    				pos=j;
    			}
    		}
    		if(pos==0){ //因为不可走,所以没找到 
     			break;
    		}
    		vis[pos]=1;
    		for(ll j=head[pos];j>=0;j=edge[j].nxt){
    			if(!vis[edge[j].to] && !sign[edge[j].to]){
    				dist[edge[j].to]=min(dist[edge[j].to],dist[pos]+edge[j].weight);
    			}
    		}
    	}
    	return dist[m];
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].nxt=head[x];
    	edge[cnt].weight=z;
    	head[x]=cnt++;
    }
    
  • P1073 最优贸易

    题目大意:有一张有向图,每个点有点权,非负。图只能从1号点开始走,最终走到n号点,可以重复经过每个点。现在希望在图中找两个点,使得从第一个选的点可以走到第二个选的点,且第二个选的点的点权-第一个选的点的点权最大。

    这是一道好题

    看到题目之后,可以想到如果是无向图,那么先用并查集看一下连通性,然后在1和n都能到达的点的集合里面选择最小点权和最大点权就好了。这道题是有向图,但是我们还是可以往这个方向去思考一下。可能我们需要看看1号点能够到达的点里面比较小的点权的点,还有该点能到达的点里面点权较大的点。虽然1号点可能到达点权很小的点,但是若这个点最后没法到比较大的点权的点的话,也不能作为答案。由于可以重复访问,所以一般的dfs和bfs也不太好处理(一般我们的dfs和bfs都不允许出现重复访问的)。

    稍微修改一下思路,或许可以把所有的点按照点权排序,用类似尺取法的方式做,不过也容易发现这样没法自然单调递减的一串答案,必须依赖优先队列这种数据结构进行调整,一旦这道题没解,那么优先队列里面将进入过多的元素而超时,且check这两个点是否合法也需要分别看1能否到第一个选点,第一个选点能否到第二个选点,第二个选点能否到达n号点,但我们没法用并查集这种手段来快速判断,目前我也不知道有没有其他东西能快速做这件事,所以这种思路似乎也没法进行。

    看了题解,发现打开了好几扇新世界的大门。

    如果不考虑极限数据,有一种思路是:进行一个特殊的dfs,这个dfs虽然最开始会标记一个点走过了,但是在某些情况下我们可以把访问标记去掉。什么时候去掉呢?我们考虑从1开始走,想记录走过的路上的最小的点权。虽然1能到的所有点里的最小的点权可能不能用,但是在找的过程中,我们有一个一步一步减小点权的过程。我们开一个数组,记录dfs到某个点时,走过的路径上的最小点权,初始化时数组为INF。考虑一件事情:假设现在走到了\(i\)结点,然后我们发现可以刷新该数组的值,如果设\(f[i]\)表示dfs到\(i\)点时的最大收益,可以知道转移方程是\(f[i]=max\{f[i],f[prev],weight[i]-minweight\}\) (其中\(prev\)是比\(i\)先dfs到的点,\(minweight\)是路上遇到的最小点权,由于可以重复走,所以\(prev\)不一定唯一)。从\(i\)开始再往后dfs的时候,由于那个路径最小值是热乎乎刚出炉的,所以一定是可以把后面的最大收益刷新的;如果走到\(i\)结点时发现不能刷新那个数组里的值了,那么以\(i\)点再进行dfs,无非还是之前对\(i\)进行dfs的那些情况,是徒劳的,不会再刷新后面那些点的最大收益了。并且,如果\(f[i]\)没有刷新,那么通过\(i\)再dfs的话,根据转移方程,后面的点的最大收益也不会刷新,所以就不用对\(i\)进行dfs了。这个方法据说可能会被卡到$O(n^2) $ ,并且不太好理解。

    本思路代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 99999999999
    using namespace std;
    const int N=1e5+9;
    const int M=1e6+9;
    typedef struct{
    	ll to,nxt;
    }Edge;
    Edge edge[M];
    typedef struct{
    	ll weight,minweight;
    }Vertex;
    Vertex v[N];
    ll head[N],cnt;
    ll f[N],n,m;
    void add(ll x,ll y);
    void dfs(ll now,ll prev,ll minw);
    int main(){
    	ll x,y,z;
    	scanf("%lld %lld",&n,&m);
    	for(int i=1;i<=n;i++){
    		scanf("%lld",&z);
    		v[i].weight=z;
    		v[i].minweight=INF;
    		head[i]=-1;
    	}
    	while(m--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y);
    		if(z==2){
    			add(y,x);
    		}
    	}
    	dfs(1,0,INF);
    	printf("%lld\n",f[n]);
    	return 0;
    } 
    void dfs(ll now,ll prev,ll minw){
    	int vis=1;
    	minw=min(minw,v[now].weight);
    	if(minw<v[now].minweight){ //minweight以后变不变 
    		v[now].minweight=minw;
    		vis=0;
    	}
    	ll tmp=max(f[prev],v[now].weight-minw);
    	if(f[now]<tmp){
    		vis=0;
    		f[now]=tmp;
    	} 
    	if(vis) return;
    	for(ll i=head[now];i>=0;i=edge[i].nxt){
    		dfs(edge[i].to,now,minw);
    	}
    }
    inline void add(ll x,ll y){
    	edge[cnt].to=y;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    

    通过看题解以及洛谷网校,我又学到了另一种做法:分层图。

    我们把图弄成3层的,第1层是买入之前的阶段,第2层是买入之后卖出之前的阶段,第3层是卖出之后的阶段。第1层与第2层之间的边权是负边权,代表买入花费,第2层和第3层之间的边权是正边权,代表卖出收益。这样的话,相当于在整个分层图中跑一遍最长路就可以了。跑最长路需要用到spfa,可能会被卡死,但是这个题好像不会被卡死。如果想稳重一些,可以用dijkstra(?),但我还不知道怎么用它写最长路,甚至不知道这个对不对,所以不先口胡了。

    分层图+spfa代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 99999999999
    using namespace std;
    const int M=5e5+9;
    const int N=1e5+9;
    const int K=4;
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    Edge edge[M*K*4];
    queue<ll> q;
    ll v[N],head[N*K],cnt,n,m,dist[N*K];
    void spfa();
    void add(ll x,ll y,ll z);
    int main(){
    	ll x,y,z,maxn=N*K,ans=-INF;
    	scanf("%lld %lld",&n,&m);
    	for(int i=0;i<maxn;i++){
    		head[i]=-1;
    		dist[i]=-INF;
    	}
    	for(int i=1;i<=n;i++){
    		scanf("%lld",&v[i]);
    		add(i,i+n,-v[i]);
    		add(i+n,i+2*n,v[i]);
    	}
    	while(m--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y,0);
    		add(x+n,y+n,0);
    		add(x+2*n,y+2*n,0);
    		if(z==2){
    			add(y,x,0);
    			add(y+n,x+n,0);
    			add(y+2*n,x+2*n,0);
    		}
    	}
    	spfa();
    	for(int i=1;i<=3;i++){
    		ans=max(ans,dist[i*n]);
    	}
    	printf("%lld\n",ans);
    	return 0;
    } 
    void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].weight=z;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    void spfa(){
    	ll now;
    	dist[1]=0;
    	q.push(1);
    	while(!q.empty()){
    		now=q.front();
    		q.pop();
    		for(ll i=head[now];i>=0;i=edge[i].nxt){
    			if(dist[edge[i].to]<dist[now]+edge[i].weight){
    				dist[edge[i].to]=dist[now]+edge[i].weight;
    				q.push(edge[i].to);
    			}
    		}
    	}
    }
    
  • P1807 最长路模板

    给一个有向无环图,求从结点1到结点n的最长路。

    说明一个事实:只有有向无环图才一定有最长路。无向图可以来回走,所以可能没最长路,有向有环同理,而有向无环保证了我们一定没法走回头路。那么如何求最长路呢?这里是一种dp的思想:到某个点\(i\) 的最短路,需要考虑它是由哪些点走到的,即它的所有的可能的前驱结点,如果设\(dist[i]\)为到\(i\) 的最长路长度,\(j\)取遍\(i\) 的所有前驱结点,那么转移方程是\(dist[i]=max\{dist[j]+weight[j][i]\}\) (可以类比松弛操作)。现在的问题是,怎么找到\(i\)结点的所有的前驱呢?可以利用拓扑排序来做这件事。在拓扑排序的过程中,在改变其他节点入度时,就可以顺带刷新一次\(dist\),只有所有的前驱都排序过了,才能继续往后走,这是由拓扑排序的特点决定的。

    这道题目也给了我一些启示:DAG上的dp是线性dp的一个升维版,我们可以用类似的思考方式去想

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 99999999999
    using namespace std;
    const int N=2e3;
    const int M=5e4+9;
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    Edge edge[M];
    ll head[N],cnt,n,m,dist[N],indegree[N];
    void add(ll x,ll y,ll z);
    int main(){
    	ll x,y,z,now;
    	queue<ll> q;
    	scanf("%lld %lld",&n,&m);
    	for(int i=0;i<=n;i++){
    		head[i]=-1;
    	}
    	while(m--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y,z);
    		indegree[y]++;
    	}
    	for(int i=1;i<=n;i++){
    		dist[i]=-INF;
    	}
    	dist[1]=0;
    	for(int i=1;i<=n;i++){
    		if(indegree[i]==0){
    			q.push(i);
    		}
    	}
    	while(!q.empty()){
    		now=q.front();
    		q.pop();
    		for(ll i=head[now];i>=0;i=edge[i].nxt){
    			indegree[edge[i].to]--;
    			dist[edge[i].to]=max(dist[edge[i].to],dist[now]+edge[i].weight);
    			if(indegree[edge[i].to]==0){
    				q.push(edge[i].to);
    			}
    		}
    	}
    	if(dist[n]==-INF){
    		printf("-1\n");
    	} else {
    		printf("%lld\n",dist[n]);
    	}
    	return 0;
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].weight=z;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    

    另一种求最长路的方法是Bellman-Ford算法或者spfa算法,把小于号改成大于号就好了。

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 99999999999
    using namespace std;
    const int N=2e3;
    const int M=5e4+9;
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    Edge edge[M];
    ll head[N],cnt,n,m,dist[N];
    void add(ll x,ll y,ll z);
    int main(){
    	ll x,y,z,now;
    	queue<ll> q;
    	scanf("%lld%lld",&n,&m);
    	for(int i=0;i<=n;i++){
    		head[i]=-1;
    		dist[i]=-INF;
    	}
    	while(m--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y,z);
    	}
    	dist[1]=0;
    	q.push(1);
    	while(!q.empty()){
    		now=q.front();
    		q.pop();
    		for(ll i=head[now];i>=0;i=edge[i].nxt){
    			if(dist[edge[i].to]<dist[now]+edge[i].weight){ //松弛成功则入队 
    				dist[edge[i].to]=dist[now]+edge[i].weight;
    				q.push(edge[i].to);
    			}
    		}
    	}
    	if(dist[n]!=-INF){
    		printf("%lld\n",dist[n]);
    	} else {
    		printf("-1\n");
    	}
    	return 0;
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].weight=z;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    
  • P2929 分层图模板

    分层图的模板是这样一个问题:有一张图,现在给你k次机会,可以对图中某些边的权值进行一些特殊操作,求操作了之后的最短路。看到这个问题,应该能想到之前做的一个dp题,题面大概是有k次操作能让物品的价值翻倍,问背包存放物品价值最大值(不过这两个问题关系不是那么大)。由于有k次机会,所以,我们可以考虑将图再复制k份,这样就有k+1份图,分别代表进行0、1、2……k次操作后要走的图。图的内部的点呀边权呀都不变,我们要考虑的是在图与图之间建立一些边来代表某条边被操作了。比如某条边e,连接i和j两个点,我们想操作这条边,比如让边权值减半,假设当前图是第0层图,也就是没进行过任何操作的图,那么我们可以在第0和第1层图之间连接这样一条边:第0层的i和第1层的j连接一条边,边权是第0层的边e的一半。需要注意,层与层之间的边可能是单向边,也就是说,有可能只能从底层向顶层移动,而没法反着移动,具体要根据题意来判断。这样的话,跑dijkstra一遍,然后看看各层的dist[n],取最小的一个就好了。值得注意的是,分层图存储时的边的数组大小要开对,或者用vector,如果是数组存的话,注意开4\(*\)层数\(*\)边数的空间

    分层图成功地化解了那些看起来很特殊的操作,使得我们可以正常跑那些图上的算法

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 999999999999999
    using namespace std;
    const int N=1e4+9;
    const int M=1e5+9;
    const int K=23;
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    typedef struct{
    	ll v,d;
    }Node;
    Edge edge[K*M*4];
    ll head[N*K],cnt,n,m,k,dist[N*K];
    priority_queue<Node> q;
    bool sign[N*K];
    void dijkstra();
    void add(ll x,ll y,ll z);
    int main(){
    	ll x,y,z,ans=INF;
    	scanf("%lld %lld %lld",&n,&m,&k);
    	for(int i=0;i<N*K;i++){
    		head[i]=-1;
    	}
    	while(m--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		add(x,y,z);
    		add(y,x,z);
    		for(ll i=1;i<=k;i++){
    			add(i*n+x,i*n+y,z); //复制的其它层的边 
    			add(i*n+y,i*n+x,z);
    			add((i-1)*n+x,i*n+y,0); //层间的边 
    			add((i-1)*n+y,i*n+x,0);
    		}
    	}
    	dijkstra();
    	for(ll i=0;i<=k;i++){
    		ans=min(ans,dist[i*n+n]);
    	}
    	printf("%lld\n",ans);
    	return 0;
    }
    void dijkstra(){
    	Node tmp,now;
    	tmp.v=1;
    	tmp.d=0;
    	q.push(tmp);
    	for(int i=1;i<=n;i++){
    		for(int j=0;j<=k;j++){
    			dist[i+j*n]=INF;
    		}
    	}
    	dist[1]=0;
    	while(!q.empty()){
    		now=q.top();
    		q.pop();
    		if(sign[now.v]) continue;
    		sign[now.v]=true; 
    		for(ll i=head[now.v];i>=0;i=edge[i].nxt){
    			if(!sign[edge[i].to] && dist[edge[i].to]>dist[now.v]+edge[i].weight){
    				dist[edge[i].to]=dist[now.v]+edge[i].weight;
    				tmp.v=edge[i].to;
    				tmp.d=dist[edge[i].to];
    				q.push(tmp);
    			}
    		}
    	}
    }
    bool operator <(const Node &a,const Node &b){
    	return a.d>=b.d;
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].weight=z;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    
  • P1144 最短路计数

    题目大意:无向图无权图,统计从1号点开始到其他各个点的最短路有多少条。

    我的想法:先跑一遍最短路,求出来到每个点最短路的长度,然后进行一波dfs,记录dfs过程中经过的点,压入stack,同时dfs过程中不断刷新最短路的条数(由于stack,所以不会出现反向刷新的情况)

    这个思路是正确的,但是,很慢(dfs),以至于全体TLE。

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 999999999999999
    #define mod 100003
    using namespace std;
    const int N=1e6+9;
    const int M=2e6+9;
    typedef struct{
    	ll to,nxt;
    }Edge;
    typedef struct{
    	ll pos,len;
    }Node;
    Edge edge[2*M];
    ll head[N],cnt,n,m,dist[N],f[N];
    bool vis[N],instack[N];
    priority_queue<Node> q;
    void add(ll x,ll y);
    void dijkstra();
    void dfs(ll now,ll nowlen,ll fa);
    int main(){
    	ll x,y;
    	scanf("%lld %lld",&n,&m);
    	for(int i=0;i<=n;i++){
    		head[i]=-1;
    	}
    	while(m--){
    		scanf("%lld %lld",&x,&y);
    		add(x,y);
    		add(y,x);
    	}
    	dijkstra();
    	f[1]=1;
    	instack[1]=true;
    	for(ll i=head[1];i>=0;i=edge[i].nxt){
    		dfs(edge[i].to,0,1);
    	}
    	for(ll i=1;i<=n;i++){
    		printf("%lld\n",f[i]);
    	}
    	return 0;
    } 
    bool operator <(const Node &a,const Node &b){
    	return a.len>b.len;
    }
    void dfs(ll now,ll nowlen,ll fa){
    	if(dist[now]==nowlen+1){
    		f[now]=(f[now]+1)%mod;
    	}
    	for(ll i=head[now];i>=0;i=edge[i].nxt){
    		if(!instack[edge[i].to]){
    			instack[edge[i].to]=true;
    			dfs(edge[i].to,nowlen+1,now);
    			instack[edge[i].to]=false;
    		}
    	}
    }
    void dijkstra(){
    	for(int i=0;i<=n;i++){
    		dist[i]=INF;
    	}
    	dist[1]=0;
    	Node now,tmp;
    	now.len=0;
    	now.pos=1;
    	q.push(now);
    	while(!q.empty()){
    		now=q.top();
    		q.pop();
    		if(vis[now.pos]) continue;
    		vis[now.pos]=true;
    		for(ll i=head[now.pos];i>=0;i=edge[i].nxt){
    			if(dist[edge[i].to]>dist[now.pos]+1){
    				dist[edge[i].to]=dist[now.pos]+1;
    				tmp.len=dist[edge[i].to];
    				tmp.pos=edge[i].to;
    				q.push(tmp);
    			}
    		}
    	}
    }
    void add(ll x,ll y){
    	edge[cnt].to=y;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    

    考虑优化,原来的dfs慢,是因为没有充分利用之前算的结果。如果能记录到达某个点的最短路条数\(f[i]\),那么当由点\(j\)能到达\(i\)的时候,如果\(f[i]!=0\),那么\(f[j]+=f[i]\)就好了。其实我本来的想法就是这样的,但是写的时候实现错误,导致结果不对,所以改成了暴力。其实就应该记忆化搜索的,或者拓扑排序+dp。

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 999999999999999
    #define mod 100003
    using namespace std;
    const int N=1e6+9;
    const int M=2e6+9;
    typedef struct{
    	ll to,nxt;
    }Edge;
    typedef struct{
    	ll pos,len;
    }Node;
    Edge edge[2*M];
    ll head[N],cnt,n,m,dist[N],f[N];
    bool vis[N],instack[N];
    priority_queue<Node> q;
    void add(ll x,ll y);
    void dijkstra();
    ll dfs(ll now);
    int main(){
    	ll x,y;
    	scanf("%lld %lld",&n,&m);
    	for(int i=0;i<=n;i++){
    		head[i]=-1;
    	}
    	while(m--){
    		scanf("%lld %lld",&x,&y);
    		add(x,y);
    		add(y,x);
    	}
    	dijkstra();
    	f[1]=1;
    	for(ll i=1;i<=n;i++){
    		printf("%lld\n",dfs(i));
    	}
    	return 0;
    } 
    bool operator <(const Node &a,const Node &b){
    	return a.len>b.len;
    }
    ll dfs(ll now){
    	if(f[now]) return f[now];
    	for(ll i=head[now];i>=0;i=edge[i].nxt){
    		if(dist[edge[i].to]==dist[now]-1){
    			f[now]=(f[now]+dfs(edge[i].to))%mod;
    		}
    	}
    	return f[now];
    }
    void dijkstra(){
    	for(int i=0;i<=n;i++){
    		dist[i]=INF;
    	}
    	dist[1]=0;
    	Node now,tmp;
    	now.len=0;
    	now.pos=1;
    	q.push(now);
    	while(!q.empty()){
    		now=q.top();
    		q.pop();
    		if(vis[now.pos]) continue;
    		vis[now.pos]=true;
    		for(ll i=head[now.pos];i>=0;i=edge[i].nxt){
    			if(dist[edge[i].to]>dist[now.pos]+1){
    				dist[edge[i].to]=dist[now.pos]+1;
    				tmp.len=dist[edge[i].to];
    				tmp.pos=edge[i].to;
    				q.push(tmp);
    			}
    		}
    	}
    }
    void add(ll x,ll y){
    	edge[cnt].to=y;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    

    虽然上述办法可以AC,但是还是不够好,因为它进行了一次记忆化搜索。能不能不用记忆化搜索呢?如果可以不用的话,那么一定是在求最短路的时候顺手统计一下路径条数。

    考虑dijkstra,当拿到一个之前没找到最短路,现在可以作为最短路的点之后,我们是要进行松弛操作的。记当前点叫做\(now\)\(now\)可以到达点\(to\),则,若\(dist[now]+weight[now][to]<dist[to]\)的话,除了松弛,我们还要令\(f[to]=f[now]\),因为\(f[to]\)之前的统计都不能要了;若\(dist[now]+weight[now][to]=dist[to]\),那么我们就\(f[to]+=f[now]\),来增加最短路径条数。这样的话,相当于把记忆化/dp放到了最短路算法里面。

    代码不贴了,和下一个题的一样。

  • P1608 路径统计

    题目大意:上面那个题的加强版,有向图,有边权,统计从1号点出发到n号点的最短路的条数。

    如果这个题的图是一个DAG,那么完全可以toposort+dp。但是这个题并不能保证是一个DAG,所以我们可能要先考虑缩点。缩点的话,会出一个问题:如果n号点和其他点被缩在一起了,记为点n',那么该怎么判断某条路是不是最短路呢?似乎是不可做的,我们放弃缩点的方法。

    再思考能否用上一个题的办法:在做最短路的同时进行统计最短路条数。这是可以的,和上一个题基本一样。

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 999999999999
    using namespace std;
    const int N=2e3+9;
    ll graph[N][N],dist[N],f[N];
    bool sign[N];
    ll n,m;
    void dijkstra();
    int main(){
    	ll x,y,z;
    	scanf("%lld %lld",&n,&m);
    	for(ll i=1;i<=n;i++){
    		for(ll j=1;j<=n;j++){
    			graph[i][j]=INF;
    		}
    	}
    //	for(ll i=1;i<=n;i++){
    //		graph[i][i]=0;
    //	}
    	while(m--){
    		scanf("%lld %lld %lld",&x,&y,&z);
    		graph[x][y]=min(graph[x][y],z);
    	}
    	dijkstra();
    	if(dist[n]==INF){
    		printf("No answer\n");
    	} else {
    		printf("%lld %lld\n",dist[n],f[n]);
    	}
    	return 0;
    }
    void dijkstra(){
    	ll minlen=INF;
    	ll minpos=0;
    	dist[1]=0;
    	f[1]=1;
    	for(int i=2;i<=n;i++){
    		dist[i]=INF;
    	}
    	for(ll i=1;i<=n;i++){
    		minpos=0;
    		minlen=INF;
    		for(ll j=1;j<=n;j++){
    			if(dist[j]<minlen && !sign[j]){
    				minlen=dist[j];
    				minpos=j;
    			}
    		}
    		if(minpos==0) break;
    		sign[minpos]=true;
    		for(ll j=1;j<=n;j++){
    			if(dist[j]>dist[minpos]+graph[minpos][j]){
    				dist[j]=dist[minpos]+graph[minpos][j];
    				f[j]=f[minpos];
    			} else if(dist[j]==dist[minpos]+graph[minpos][j]){
    				f[j]=f[j]+f[minpos];
    			}
    		}
    	}
    }
    
  • P3953 逛公园

    题目大意:上面那个题的拓展,有向图,有边权,如果从1号点出发到n号点最短路径长度为d,那么统计从1号点出发到n号点的长度不超过d+k的路径的条数。

    经验:记忆化搜索是图论题中常用的方法,思维难度远低于拓扑+dp

    本题不是对最短路进行计数,而是对满足条件的比较短的路进行计数,这样的话就没办法在跑最短路算法的同时不断更新答案了。不过,看起来上上道题的记忆化搜索可能还是可以的。假设当前点为\(i\)\(i\)现在要走到点\(j\),如果定义\(f[i]\)表示从1号点走到\(i\)号点的长度小于等于\(d+k\)的路径的条数的话,考虑转移,不难发现,要想在已知\(f[i]\)的情况下转移到\(f[j]\),需要知道\(i,j\)的边长以及\(f[i]\)的具体细节,即长度为\(len\)的路有多少条,这样才能准确转移。为了记录这个信息,考虑给状态加一维,设\(f[i][len]\)为从1号点走到\(i\)号点,且长度小于等于\(len\)的路径的条数。我们最后要求的,就是\(f[n][d+k]\),为了得到这个数,我们要考虑\(n\)的所有前驱点。假设有一个前驱点为\(i\)\(i,n\)的距离是\(w\),那么我们只需要知道\(f[i][d+k-len]\),就可以进行转移了,即\(f[n][d+k]+=f[i][d+k-len]\),进一步考虑\(f[i][d+k-len]\)是怎么来的,我们需要考虑\(i\)的前驱,然后用类似的转移方式进行转移。这样看来,对于每个点\(i\),我们只需要求很少的几个\(f[i][len]\),用到哪个就求哪个,似乎空间上是可以优化的(显然,刚才的二维状态是没法开二维数组的,因为\(n\)已经很大,而\(d+k\)还能更大)。一种优化空间的思路是,开一维的\(f[]\)数组,并将\(f[]\)数组中的元素的类型设定为map,记忆化搜索的时候对于\(f[i]\),先在map里面查一下有没有相应的\(len\)键,有的话就直接返回其值,没有的话就记搜算出来,并在map中加入这个键值对(STL好香啊)。

    这个方法期望得分70分,实际得分37分,因为不明原因WA掉,WA掉的测试点都是k不为0的测试点,大胆猜测是因为记忆化搜索的边界条件没有处理好,但是我对拍难以生成hack数据。

    另外值得注意的是,有向图跑记忆化搜索时,有可能要先建反图,否则没法从后面的阶段追溯前面阶段的解

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 9999999999999
    using namespace std;
    const int N=1e5+9;
    const int M=2e5+9;
    const int K=52;
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    typedef struct{
    	ll pos,len;
    }Node;
    Edge edge[M],redge[M];
    map<ll,ll> f[N]; //长度为len的边的条数 
    ll head[N],rhead[N],cnt,rcnt,n,m,k,p,t,dist[N];
    bool sign[N];
    priority_queue<Node> q;
    void dijkstra();
    ll dfs(ll now,ll len);
    void add(ll x,ll y,ll z);
    void radd(ll x,ll y,ll z);
    int main(){
    	freopen("in.txt","r",stdin); 
    	ll x,y,z;
    	scanf("%lld",&t);
    	while(t--){
    		scanf("%lld %lld %lld %lld",&n,&m,&k,&p);
    		cnt=0;
    		rcnt=0;
    		for(ll i=0;i<=n;i++){
    			head[i]=-1;
    			rhead[i]=-1;
    		}
    		for(ll i=1;i<=n;i++){
    			f[i].erase(f[i].begin(),f[i].end());
    		}
    		while(m--){
    			scanf("%lld %lld %lld",&x,&y,&z);
    			add(x,y,z);
    			radd(y,x,z); //建反图,以进行记忆化搜索 
    		}
    		dijkstra();
    		printf("%lld\n",dfs(n,dist[n]+k));
    	}
    	return 0;
    }
    ll dfs(ll now,ll len){
    	//boundary condition
    	ll minlen=INF;
    	ll ans=0;
    	map<ll,ll>::iterator it;
    	it=f[now].find(len); //找到长不超过len的路径条数 
    	if(it!=f[now].end()){ //如果之前算过了 
    		ans=it->second; 
    		return ans;
    	}
    	for(ll i=rhead[now];i>=0;i=redge[i].nxt){
    		minlen=min(minlen,redge[i].weight);
    		if(len-redge[i].weight>=0){
    			ans=(ans+dfs(redge[i].to,len-redge[i].weight))%p;
    		}
    	}
    	if(ans==0 && now==1){ //如果搜到了起点,没搜出来路,且确实是因为len太小所以没搜出来 
    		f[now][len]=1;
    		return 1;
    	}
    	f[now][len]=ans;  //记忆化 
    	return ans;
    }
    void dijkstra(){
    	Node tmp,now;
    	for(ll i=1;i<=n;i++){
    		dist[i]=INF;
    		sign[i]=false;
    	}
    	while(!q.empty()){
    		q.pop();
    	}
    	dist[1]=0;
    	now.pos=1;
    	now.len=0;
    	q.push(now);
    	while(!q.empty()){
    		now=q.top();
    		q.pop();
    		if(sign[now.pos]) continue;
    		sign[now.pos]=true;
    		for(ll i=head[now.pos];i>=0;i=edge[i].nxt){
    			if(dist[edge[i].to]>dist[now.pos]+edge[i].weight){
    				dist[edge[i].to]=dist[now.pos]+edge[i].weight;
    				tmp.pos=edge[i].to;
    				tmp.len=dist[edge[i].to];
    				q.push(tmp);
    			}
    		}
    	}
    }
    bool operator <(const Node &a,const Node &b){
    	return a.len>b.len;
    }
    inline void radd(ll x,ll y,ll z){
    	redge[rcnt].to=y;
    	redge[rcnt].weight=z;
    	redge[rcnt].nxt=rhead[x];
    	rhead[x]=rcnt++;
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].weight=z;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    

    由于又是TLE又是MLE的,所以上面那个实在不太行。然后我看了题解,发现上面我的做法的状态设计可以优化一下,这样就不需要用map了。定义\(f[i][j]\)为从1开始走到\(i\),并且走的路比从1到\(i\)的最短路长不超过\(j\)的路径的条数。这样的话,最后就是求\(f[n][k]\)。由于k比较小,所以二维数组可以开的下,不需要用map了。如果这样定义状态,考虑状态转移:假设有一条边是\((u,v,w)\),并且已知\(f[u][j]\)(从1开始走到\(u\),并且路径长度比二者之间最短路径长度长不超过\(j\)的路径条数),那么,在走了这条边之后,多走的路应该是小于等于\(mindist(1,u)+j+w-mindist(1,v)\)的,如果这个数比\(k\)小,那么就可以转移到

    \(f[v][mindist(1,u)+j+w-mindist(1,v)]\) 了。考虑初始化,显然\(f[1][0]\)很好算,那\(f[1][k]\)就一定是1吗?不一定。如果1号点在一个环上,然后整个环跑一圈,长度都不超过\(k\) ,那么方案数就不是1了。如何避免出现这个问题呢?我们先考虑只初始化\(f[1][0]\),然后想想怎么枚举循环变量进行转移。显然要按照拓扑关系进行转移,可是这个图不保证没有环,所以也没法进行拓扑排序,怎么办呢?还是记忆化搜索吧,太香了。

    在记忆化搜索时,还是遇到了那个老问题:如果有\(f[1][k]\)不是1的情况怎么办?又看题解,修改状态定义,这次定义定义\(f[i][j]\)为从1开始走到\(i\),并且走的路比从1到\(i\)的最短路长\(j\) 的路径的条数,这样的话,记忆化搜索的边界条件设成如果当前搜索的是1号点,并且可松弛长度为0时,就可以return 1了,完美解决那个问题。这个地方体现了状态定义对于边界处理的作用

    当然,这样没法通过有0环的数据。如果有0环的话,说明我们现在在算某个状态时,发现它本身的结果又依赖于它本身的结果,那样就是在0环上跑了,就无解了。在这个功能的实现上,我们利用栈的思想,把当前状态入栈,然后如果发现转移到了当前还在栈中的状态,那么就说明跑到了影响结果的0环,就无解了,我们就一直返回EOF直到退出dfs就好了。

    这个题我吸氧+前置加+重载小于号为大于等于+读入优化+内联+减少取模次数才卡常勉强通过此题,太难了!

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 9999999999999
    #define INFF 9999999999
    using namespace std;
    const int N=1e5+9;
    const int M=2e5+9;
    const int K=52;
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    typedef struct{
    	ll pos,len;
    }Node;
    Edge edge[M],redge[M];
    ll head[N],rhead[N],cnt,rcnt,n,m,k,p,t,dist[N],f[N][K];
    bool sign[N];
    bool flag[N][K];
    priority_queue<Node> q;
    void dijkstra();
    ll read();
    void add(ll x,ll y,ll z);
    void radd(ll x,ll y,ll z);
    ll dfs(ll now,ll extra);
    int main(){
    //	freopen("in.txt","r",stdin);
    	ll x,y,z,ans,tmp;
    	t=read();
    //	scanf("%lld",&t);
    	while(t--){
    		n=read();
    		m=read();
    		k=read();
    		p=read();
    //		scanf("%lld %lld %lld %lld",&n,&m,&k,&p);
    		ans=0;
    		cnt=0;
    		rcnt=0;
    		for(ll i=0;i<=n;++i){
    			head[i]=-1;
    			rhead[i]=-1;
    		}
    		for(ll i=0;i<=n;++i){
    			for(ll j=0;j<K;++j){
    				f[i][j]=0;
    				flag[i][j]=false;
    			}
    		}
    		while(m--){
    			x=read();
    			y=read();
    			z=read(); 
    //			scanf("%lld %lld %lld",&x,&y,&z);
    			add(x,y,z);
    			radd(y,x,z); //建反图,以进行记忆化搜索 
    		}
    		dijkstra();
    		for(ll i=0;i<=k;++i){
    			tmp=dfs(n,i);
    			if(tmp==-1) break;
    			ans=(ans+tmp)%p;
    		}
    		if(tmp==-1){
    			printf("-1\n");
    		} else {
    			printf("%lld\n",ans);
    		}
    	}
    	return 0;
    }
    inline ll read() {
        char ch = getchar();ll ret=0;
        while(ch<'0'||ch >'9') ch=getchar();
        while(ch<='9'&&ch>='0') ret=ret*10+ch-'0',ch=getchar();
        return ret;
    }
    inline ll dfs(ll now,ll extra){
    	ll tmp=0,ret=0;
    	if(extra<0 || extra>k) return 0;
    	if(flag[now][extra]) return -1;
    	if(f[now][extra]) return f[now][extra];
    	flag[now][extra]=true; //状态入栈 
    	for(ll i=rhead[now];i>=0;i=redge[i].nxt){
    		tmp=dfs(redge[i].to,extra+dist[now]-redge[i].weight-dist[redge[i].to]);
    		if(tmp!=-1){
    			ret=(ret+tmp);
    			if(ret>INFF){
    				ret=ret%p;
    			}
    		} else {
    			return -1;
    		}
    	}
    	if(now==1 && extra==0){
    		ret++;
    	}
    	f[now][extra]=ret;
    	flag[now][extra]=false; //状态出栈 
    	return f[now][extra];
    }
    inline void dijkstra(){
    	Node tmp,now;
    	for(ll i=1;i<=n;++i){
    		dist[i]=INF;
    		sign[i]=false;
    	}
    	while(!q.empty()){
    		q.pop();
    	}
    	dist[1]=0;
    	now.pos=1;
    	now.len=0;
    	q.push(now);
    	while(!q.empty()){
    		now=q.top();
    		q.pop();
    		if(sign[now.pos]) continue;
    		sign[now.pos]=true;
    		for(ll i=head[now.pos];i>=0;i=edge[i].nxt){
    			if(dist[edge[i].to]>dist[now.pos]+edge[i].weight){
    				dist[edge[i].to]=dist[now.pos]+edge[i].weight;
    				tmp.pos=edge[i].to;
    				tmp.len=dist[edge[i].to];
    				q.push(tmp);
    			}
    		}
    	}
    }
    bool operator <(const Node &a,const Node &b){
    	return a.len>=b.len;
    }
    inline void radd(ll x,ll y,ll z){
    	redge[rcnt].to=y;
    	redge[rcnt].weight=z;
    	redge[rcnt].nxt=rhead[x];
    	rhead[x]=rcnt++;
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].weight=z;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    
  • 吃鸡

    这是一道牛客的题目,给了我一定的启示:为了设计更优的算法,需要考虑将条件用数学式子表达。

    题目大意:有一张带权无向图,有一个人想从s点走到t点,并且只走最短路。现在我们想取两个点A,B,这两个点满足:对于这个人走的任何一条最短路,A和B中都有且仅有一个点在这条最短路上。求这样的点对A,B有多少组。

    通过模拟样例,我们发现,对于那种两条链合成一个环的图,只要在两条链上分别找点,就能凑出来所有的点对。如果有三条甚至以上的独立的最短路能从s到t,那么这样的点对就不存在了。要想有这样的点对,最短路必须最后会合成两条链。

    记录最短路的方法是记录每个点是由哪个点松弛才得到最短路的(很久没写了,加粗强调一下)。

    有刚才的分析,假如这种点对存在,一个可能的解决方案是,把最短路会合之前的环给缩一下点。如果把那些环都给缩了,那么最后走的路径的样子差不多就是两条链,然后在这两条链上直接算就好了。然而我不知道咋在无向图里面“缩点”。

    经过思考之后,看了题解,发现了一种不需要知道已经经过了哪些点,就能知道有多少对符合要求的点的方法。我们想一下从s经过某个点x,然后再到t的最短路的条数怎么求。容易发现,由乘法原理知,就是从s到x的最短路的条数\(*\)从x到t的最短路的条数。由于是无向图,所以也可以是从s到x的最短路的条数\(*\)从t到x的最短路的条数。我们想一下,题目中说A和B这两个点必须有且仅有一个能在从s到t的最短路上,所以从s到t的最短路的条数=经过A的+经过B的。记\(f[i][j]\)为从\(i\)\(j\)的最短路条数,那么条件转化为数学式子就是\(f[s][t]=f[s][A]*f[A][t]+f[s][B]*f[B][t]\) 。为了方便,我们正反跑两次dijkstra去算出来这些数值。为了减少枚举A和B,我们可能要将图删去一部分,只保留一个最短路径图。

  • 区间最短路 && 线段树优化建图

    这道题主要想讲一下在图的边数相当多时如何加快建图。

    这道题目涉及到一个点向一个区间内所有的点连接边,如果每次都是一个点向所有边连接的话,最后会得到一个稠密图,这会导致堆优化dijkstra和spfa都会跑得很慢,并且能不能存的下还是个问题。除此之外,我们容易发现,建图的过程本身就是一个二重循环,很可能图还没有建完就超时了。我们需要想一个办法来加快建图。

    根据提示,可以想到,如果用一个结点代表某个区间,那么在连接从一个点到一个区间的边的时候,可以只连这个点到这个代表区间的点的边,然后这个区间再向他代表的所有的点连接0权边,但是这样还白白搭上了一条边,难受。这时候我们提出线段树优化建图的方法,即先把整个\([1,n]\)的线段树预处理出来,预处理的目的是,把线段树的边存到边集数组里面(全都是0权边),并且给线段树中的点标号,从\(n+1\)开始标(因为\(1-n\)都已经确定好了,就是题目中说到的结点)。为了连续地给线段树的结点标号,这里的线段树\(i\)结点的左右儿子就不一定是\(2i\)\(2i+1\)了,需要单独存储一下,用\(lchild[]\)\(rchild[]\)存储。预处理结束之后,我们读入建图的数据,然后把涉及到的区间分解成极大区间,连上边就好了。

    (下面的内容有待商榷)

    对于线段树建图的额外消耗,我们知道,对于一个长度为n的区间,一般需要开4倍左右的空间来存储,虽然在这里存储的时候没有最后一层的额外消耗了,但还是开4倍空间比较好。因为还有从区间到点的边,所以还要再建一个线段树,这样的话额外消耗就是两棵线段树,再加上原空间,开10倍的点的空间完全够了(其实完全用不到那么多空间)。关于边的空间,我们每个长度为n的区间大约被分成\(log_2n\)个区间,也就需要连\(log_2n\)条边,比原来的连\(n\)条边少多了。假设有\(q\)个连接点到区间的边或者区间到点的边的操作,那么就需要多连\(qlog_2n\)条边,再加上两棵线段树本身的边需要消耗\(8n\)的边空间,开10倍的空间也差不多够了。

树论

  • P4281 紧急集合

    题目大意:树上有三个点,在树上求一个点,使得这三个点到这个点的距离之和最小。

    这个题印象深刻。树上问题一般会想dp,lca,树剖等东西。这个题目只涉及3个点,考虑到两个点的时候就是求lca,所以这个题很容易往lca上靠拢。这个题要画图之后大胆猜结论:三个点中两两求lca,能求出来3个lca,但其中肯定有两个是相同的,并且相同的这个离得比较远,另外一个lca就是我们要求的点,因为它能让两个点走得比较少,另外一个走得稍微多一点。用数学式子来说话,就是\(depth[a]+depth[b]+depth[c]-3*depth[goal]\)就是要走的路径长,我们想让\(depth[goal]\)尽可能大,所以就得选那个不同的lca。这个题我没有做出来的原因,一是没画图,光靠想的,二是猜出来结论完全没有依据,没有数学式子的支撑,所以不敢写。如果写出来上面那个式子,心里会特别有谱,或许就能够做出来了。所以,一定不要忽视数学式子呀!!

    代码几乎就是lca模板,不放出来了。

  • P5836 喝牛奶

    题目大意:查询树上路径的信息,信息在点上,信息具有结合律。

    树上路径的信息的维护,一般是思考用树剖,但是,有些时候利用“前缀和”的思想,会发现如果能预处理出所有节点到根的信息,那么可能用lca就能做,并且代码还简单,复杂度相对于树剖也更小(少一个log)。比如树上两点距离,其实完全可以设定每个点的点权为1,用树剖强行求和,然后-1就是距离了,但没必要,其实用lca一下子就能出来的。这个题也一样,要求两个点之间这条路径上有没有某个东西,东西的个数是点权信息,我们当然可以暴力树剖得到这个信息,也可以预处理出所有点到根的点权和信息之后,借助lca减去重复部分。这个题目给我的启示是,树剖可以做很多题,但不一定是最优解,或许有时候可以经过一些预处理,然后用lca巧妙地进一步降低复杂度。

    代码就是lca模板,不放了。

  • P3128 树上差分模板

    点s到点t的路径上的所有点的点权+1,则只要s和t的差分+1,且lca(s,t)的差分-1,fa[lca(s,t)]的差分-1即可。最后只要“后序遍历”,先把子树的差分都加到根上,然后再用差分去更新点权就好了。

  • P5959 根据深度信息构造树

    题目大意:已知\(2 - n-1\)号结点到\(1\)\(n\)号结点的距离,根据这些信息把树构造出来,树的边是带权的。

    我先模拟了一下样例,发现不管怎么样,我最开始连的边都是距离\(1\)\(n\)距离最小的边,因为它一定是一条边的权值而不是多条边的权值和。试探性地把最短的边都连上之后,然后考虑剩余边中比较短的边,它有可能和\(1\)\(n\)单独连一条边,也可能自己自成体系,都要尝试一下。当目前确定的点构成一棵树的时候,再插点加边时,就得保证无环+距离正确了。无环可以并查集,而插点插到哪里则需要枚举可能的插入位置了,即枚举现在已经出现在树中的点,然后根据这个点到达\(1\)\(n\)的距离来确定有没有合适的权值,使得要插入的点能挂在这个点上。不过容易发现,这种做法可能被卡成\(O(n^2)\),很难受。并且,插入可能有多种可能,所以可能需要进行状态表示,然后进行bfs,但是对于一棵树来说,状态的表示是个问题,入队出队之后还原成树需要重构,也很浪费时间。

    听了老师讲了之后,才知道不需要这么麻烦的方法。我们可以考虑先构造出一条链,然后往链中加结点或者挂在链上结点。先考虑\(1\)\(n\)之间的路径长度,根据三角形两边之和大于第三边,所以,我们应该找一个点,使得\(dist(1,i)+dist(i,n)\)最小,然后,让这个点\(i\)就在\(1\)\(n\)的最短路径上,即令两边之和等于第三边(显然,考虑一下三个结点的情况,构造出来的树中\(1\)\(n\)的距离也很可能是\(|dist(1,i)-dist(i,n)|\) ,这是一个特例,需要单独考虑)。显然,所有满足\(dist(1,i)+dist(i,n)\) 的点都在主链上,但是我们不能找到之后瞬间把它加进去,因为有可能会把之前的一些边断开重连调整权值。正确做法是找到所有这样的点,按照对1的距离进行排序,然后按这个顺序在链上排点。链弄好之后,就可以枚举其他的点,然后看看该挂到哪个链上,这需要我们先算出该挂的点以及边的权值是多少,然后算出来它在主链上应该离\(1\)\(n\)多远。由于主链上的有序性,可以二分查找那个应该挂的位置。如果没找到,就说明这棵树不存在。值得注意的是,在判定为不存在的时候,要再特判一下我们上面提到的特殊情况,也就是先检测一下所有的\(|dist(1,i)-dist(i,n)|\) 是否相等,如果相等就直接把点往两边挂上就好了。

    目前这个题我有两个应该判无解的测试点但我的程序判断有解,暂时还没有通过。

  • P5994 猜猜看

    题目大意:有n个容器,每个容器有放与不放东西两种状态,但是你不知道这些容器处于什么状态。你可以作出一些询问,每次询问可以询问一个区间内的所有容器的含有多少样东西的奇偶性,但是每次询问都会有消耗。问最少消耗多少,能够确保确定每个容器的状态。

    这道题,如果单独询问每个容器,当然很好,但是这个题目设计的询问单个容器的花费很高,不划算,反而是通过多次区间询问来推断单点询问比较好。并且,我觉得如果有n个容器,那么我至少需要n次才能知道每个容器的情况。看到奇偶性,根据上次做数论题的经验,想到了异或,这个和异或可能有关系,这里访问一个区间得到奇偶性就是做一次区间异或和。

    看一下样例:

    5
    1 2 3 4 5
    4 3 2 1
    3 4 5
    2 1
    5
    

    显然,单点访问1号很好,sum=1;

    单点访问2号要消耗4,太贵了,不过考虑访问1-2,只消耗2,sum=3,只要看看1和1-2奇偶性有没有变就好了。

    单点访问3号,要消耗3,但是感觉能更少,所以先不管

    单点访问4,还行,消耗2,sum=5

    单点访问5太贵,但是访问4-5很便宜,sum=6

    考虑区间2-5,因为2和4-5都确定了,所以只要知道2-4的奇偶性,就能知道3是什么状态了,消耗1,sum=7

    这样看结果有点贪心的意思,但是我却说不上来这个贪心的策略是什么。

    不过突然,我好像醒悟了什么。考虑把\(c_{i,j}\) 看成\(i\)\(j\)连一条边,那么我们最后想求的就似乎是一个图上的最小的一个什么东西,这个最小的东西的求法可能用到什么现成的算法。这个图画出来之后,是一个\(n\)阶完全无向图+自环。但说了这么多,到底是在这个图里面求什么呢?

    我们刚才说了,两次区间访问能确定一个更小的区间甚至是单点的状态情况,即图中的每一条边,其实都可以看成是一次或者两次询问带来的结果,比如4-5这条边,可以看作是1-3和1-5这两个区间访问能够得到的信息,也可以看成2-3和2-5两个区间访问得到的,当然也可以是直接访问4-5区间的信息。这时候我们就想了,如果把这个图搞出来一个生成树,会是什么意思呢?显然这个生成树有\(n-1\)条边,这\(n-1\)条边的起点\(from\)和终点\(to\)代表了我们做了对区间\([from,to]\)的直接询问,说句废话就是对\([1,2]\) 的询问就是连接\((1,2)\)这条边,对\([2,5]\)区间的询问就是连接\((2,5)\)这条边。那么这\(n-1\)条边代表的询问能让我们知道所有节点的信息吗?是不能的。但即使是这样\(n-1\)个“方程”,也是不能解出来\(n\)条信息的,我们还需要一个方程,即还需要一条边。这样本来只要求一个MST就可以,现在却变成求一个最小的带一个环的图了,不爽。不过能分析到MST,这个题已经建模了一大半了,下面我们要做一些修修补补的工作。现在的感觉就是,我知道递推公式,但是我没有首项,要是我至少知道一个单点的情况就好了,那样肯定可做。但是,单点相当于自环,相当于单点查询,但是最开始我们就说了,单点查询可能消耗更大,并且加入自环也就不是MST了。

    这时候,注意到我们需要\(n\)个方程,但是我们也只有\(n\)个点,要是有\(n+1\)个点,MST就能直接上。这时候,我们不妨引入一个辅助容器0号,它里面什么都不放,即状态是确定的,0号容器就是我们的首项。其他任意点\(i\)和0号点直接相连都代表原来的图中的自环,即单点查询\(i\)号容器。这样,这个题就稳了,根据题意直接建一个\(n+1\)个结点的图,自环用0号结点处理掉,其他点照常连,然后跑MST就好了。因为这个是稠密图,所以要用朴素的prim算法。

    代码非常简洁:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 9999999999999
    using namespace std;
    const int N=2e3+9;
    const int M=N*(N-1);
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    Edge edge[M];
    ll head[N],cnt,n,dist[N],ans;
    bool intree[N];
    void prim();
    void add(ll x,ll y,ll z);
    int main(){
    	ll w;
    	scanf("%lld",&n);
    	for(int i=0;i<=n;i++){
    		head[i]=-1;
    	}
    	for(int i=1;i<=n;i++){
    		for(int j=i;j<=n;j++){
    			scanf("%lld",&w);
    			add(i-1,j,w);
    			add(j,i-1,w);
    		}
    	}
    	prim();
    	printf("%lld\n",ans);
    	return 0;
    }
    inline void prim(){
    	ll minlen,minpos;
    	for(int i=0;i<=n;i++){
    		dist[i]=INF;
    	}
    	dist[0]=0;
    	for(int j=0;j<=n;j++){ //循环n+1次
    		minlen=INF;
    		for(int i=0;i<=n;i++){
    			if(minlen>dist[i] && !intree[i]){
    				minlen=dist[i];
    				minpos=i;
    			}
    		}
    		intree[minpos]=true;
    		ans+=dist[minpos]; 
    		for(ll i=head[minpos];i>=0;i=edge[i].nxt){
    			if(!intree[edge[i].to])
    				dist[edge[i].to]=min(dist[edge[i].to],edge[i].weight);
    		}
    	}
    }
    inline void add(ll x,ll y,ll z){
    	edge[cnt].to=y;
    	edge[cnt].weight=z;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    
  • P3761 城市

    题目大意:有一棵带权树,现在你有机会断开树上其中一条边,并且在树上再连一条同样长度的边,使之还是一棵树,求这样操作一次之后的最小直径是多少。

    这道题目用到了一个引理:对于一棵树,选择其直径的中点为根,能够使树的深度最小。

    回到本题,显然,断开一条边之后,整棵树变成了两棵树。最糟糕的情况是,重连之后的直径是两棵树直径+新连的边的长度,比较好的情况是新的树的直径是原来两棵树直径中的较大者。先考虑链上的做法,如果两棵树都是链的话,显然,把两条链的中点连接起来是最好的,这样可以让直径最短。对于树,也类似,把两棵树的直径的中点连起来,新直径最小,原因是\(d_1+d_2+w(u,v)\)\(w(u,v)\)是定值,而\(d_1和d_2\)的最小情况可以同时取到,只要取中点就好了。所以,我们枚举要去掉的边,然后对两棵树进行dfs求直径以及直径中点,同时刷新答案就好了。

    下面说一下代码实现:暴力枚举边,使原树变成两棵树,这个怎么实现呢?注意到,我们断开的边必然是有一个起点有一个终点的(废话),所以断开之后这两个点肯定在两个不相交的集合里面,所以我们可以分别以这两个点为根,分别dfs一遍(用continue跳过那条断掉的边的访问,并借用那条边的\(edge[i].nxt\)),把离这两个点最远的点求出来,然后再分别求直径的另一个端点。这样做完之后,再分别找一下中点,然后加边,再对整棵树进行两次dfs求出直径。上面的描述中,有一个地方的实现是不那么显然的:找直径的中点,或者说,算出来半径的长度,我们不能简单地除以2。在做了模板题之后,我明白了一种求无边权树的直径中点的做法:可以记录前驱到一个path[]数组中,在算直径长度的那个dfs里面,把path[]求出来,然后从终点开始反向遍历path[],时刻判断什么时候走了一半就好了。但是现在是有边权的树,所以我们似乎必须要知道这棵树怎么来的,所以我们要对每个点维护一个到根节点的距离这个信息,然后如果跳跃之后距离没少于直径的一半,就继续跳。

    另外在题解代码里面看到一个小技巧:由于我们链式前向星加边都是两条两条加的,所以给出一条边编号为\(i\)那么反边就是\(i xor 1\) ,这个结论显然是对的。

    这道题代码我不好意思放,因为我Hack了我自己,但是却AC了,hack数据如下:

    input:
    5
    5 1 4
    1 3 5
    1 2 4
    4 2 1
    correct output:
    9
    my output:
    10
    
  • P4155 国旗计划

    题目大意:有一个环周长为m,并给出n个区间,现在想求至少需要多少个区间,才能把这个环覆盖住。为了加大难度,对于每一个1<=i<=n,如果一定要选第i个区间的话,至少要多少个区间才能覆盖这个环。

    先考虑在序列并且不必必须选某个区间的情况下怎么做,我们可以把所有的区间按照左端点进行排序,先选一个能覆盖左端点,并且右端点又最大的区间作为初始区间,显然如果不选这个区间,结果一定不会更优。第一步是容易做到的,直接遍历即可。然后,往后找左端点在初始区间,且右端点尽可能靠右的区间作为第二个区间,这一步也是容易做到的,边遍历边记忆就能做到,重复上述过程直到覆盖整个区间即可得到最终答案。

    再考虑环上的做法,一般地,解决环上的问题都是把环变成链,然后把链长延长至两倍,转化成链上的问题。首先我们选了一个初始区间(幸亏题目给了我们初始区间),然后,从这个初始区间往后用类似的方法,直到覆盖出一个区间长度大于等于m的区间就好了。不过,我们发现,这种做法是基于遍历的,也就是说,我们每换一个初始区间,都要遍历去找它的下一个最贪心的区间,而这些寻找显然是有重复的,似乎预处理一下更好。如果记录选择某个区间之后的最贪心的下一个区间的话,那就好了。这个预处理的复杂度我不太会算,但是我知道,这个肯定不会超过\(O(n^2)\)。不过,如果出题人是个狼人的话,出一组特殊的数据,即n=m,且区间之间没有任何交叉的话,那么虽然预处理可以降低到\(O(n)\),但是查询的时候则会被卡到\(O(n^2)\),对于这道题来说,是绝对过不了的。也就是说,这个做法的复杂度并不稳定。

    这个时候,我想到了一篇倍增入门的文章,于是想到可能要利用倍增的思想。考虑多维护一些信息:记录从第\(i\) 个区间走\(2^j\)步所到达的区间的编号,则在查找的时候最坏的遍历复杂度就从\(O(n)\)变成了\(O(log_2n)\) ,总的最坏复杂度也就变成了\(O(nlog_2n)\) ,可以接受了。这道题目给我的最大的启示是:有时候可以用倍增来优化遍历。另外,本题还有多个教训:不要写大常数的代码!涉及排序的题目一定要考虑是否存储原有顺序!

连通性

  • P3387 缩点

    题目描述:见原题。

    缩点,是将有向图中的环缩成一个点,使得有向图变成有向无环图DAG,那么在DAG上我们就可以做一些事情,比如拓扑排序+dp。

    实现缩点的一种算法是Tarjan算法,是基于dfs,维护点的两个属性dfn和low,并且通过dfn和low的关系来得到强连通分量,从而我们就可以进行缩点了。

    关于Tarjan算法的细节,此处不予以介绍,但是Tarjan算法的过程给了我们一个启示:图上的dfs如果用stack记录dfs中的结点的话,能够做一些事情(比如可以比较低效地进行带权最短路的计数,更高效的方法是记忆化搜索)。

  • P4782 2-SAT问题

    题目大意:有n个命题\(a_1,a_2……a_n\),给出m个形如\(a_i\bigvee a_j\)的条件,判断是否能让这m个条件同时成立,如果能,则构造出一组解。

    这个问题很有实际意义,并且在条件中只有两个变量的情况下,是可以做到快速解决的。

    首先,考虑\(a_i\bigvee a_j\) ,如果它成立,那么必有非\(a_i\)能推出\(a_j\),且非\(a_j\)能推出\(a_i\)。我们可以把“推出”这个词看做是连接一条有向边。所以,每个条件都可以翻译成两条有向边,我们能够得到一个关系图。对于这个图,我们可能会发现,其中有一些环,而环意味着可以相互推出。由蕴含运算的真值表可以知道,如果\(a_i\)\(a_j\)能相互推出,那么二者必定同为真或同为假。所以,一个环里面的所有变量取值必须要一样才行。所以,一个显然的无解的情况是:\(a_i\)和非\(a_i\)在一个环里面,那样就一定不能使得二者的真值情况一样了。排除了这种无解的可能,我们考虑先缩点。如果使用的是tarjan算法的话,那么先找出来的强连通分量,在拓扑序上是靠后的,这是由dfs的性质决定的。然后,我们考察每个\(a_i\),看看它和非\(a_i\) 是不是在同一个强连通分量中。如果都不在,那么我们可以考虑构造解了。如何构造呢?显然,对于每个\(a_i\) 及其非,由于他们所在的连通分量不同,所以他们两个有三种情况:\(a_i\) 拓扑序靠前,非\(a_i\) 拓扑序靠前,二者互不影响。对于第一种情况,由于\(a_i\)靠前,且非\(a_i\)\(a_i\)真假相反,且\(a_i\) 能推出非\(a_i\) ,所以\(a_i\) 只能取假;第二种情况正好反过来了,\(a_i\)取真就好了;第三种情况,说实话我没想明白,但是根据结果来看,仍然可以按照拓扑序(编号)的大小来归结为前两种情况。所以,构造解的过程就是,遍历\(a_i\),比较\(a_i\)和非\(a_i\) 的拓扑序,确定取值,完了。

    这道题目的启示是:可以用图表示一些约束条件,通过思考图中的点、边、路径、环等的实际意义,可能能利用图论算法解决问题

    代码如下:

    #include <bits/stdc++.h>
    #define ll long long
    #define INF 99999999999
    using namespace std;
    const int N=2e6+9;
    const int M=2e6+9; 
    typedef struct{
    	ll to,nxt,weight;
    }Edge;
    Edge edge[M];
    stack<ll> s;
    bool instack[N];
    ll n,m,n2,head[N],cnt,dfscnt,low[N],dfn[N],scccnt,color[N];
    void add(ll x,ll y);
    void tarjan(ll now);
    int main(){
    	ll p,q,a,b;
    	scanf("%lld %lld",&n,&m);
    	n2=2*n;
    	for(int i=0;i<=n2;i++){
    		head[i]=-1;
    	}
    	for(int i=0;i<m;i++){
    		scanf("%lld %lld %lld %lld",&p,&a,&q,&b);
    		if(a==0 && b==0){
    			add(p,q+n);
    			add(q,p+n);
    		} else if(a==0 && b==1) {
    			add(p,q);
    			add(q+n,p+n);
    		} else if(a==1 && b==0) {
    			add(p+n,q+n);
    			add(q,p);
    		} else if(a==1 && b==1) {
    			add(p+n,q);
    			add(q+n,p);
    		}
    	}
    	for(int i=1;i<=n2;i++){
    		if(!dfn[i]){
    			tarjan(i);
    		}
    	}
    	for(int i=1;i<=n;i++){
    		if(color[i]==color[i+n]){
    			printf("IMPOSSIBLE\n");
    			return 0;
    		}
    	}
    	printf("POSSIBLE\n");
    	for(int i=1;i<=n;i++){
    		if(color[i]<color[i+n]){
    			printf("1 ");
    		} else {
    			printf("0 ");
    		}
    	}
    	return 0;
    }
    void add(ll x,ll y){
    	edge[cnt].to=y;
    	edge[cnt].nxt=head[x];
    	head[x]=cnt++;
    }
    void tarjan(ll now){
    	dfn[now]=++dfscnt;
    	low[now]=dfscnt;
    	instack[now]=true;
    	s.push(now);
    	for(ll i=head[now];i>=0;i=edge[i].nxt){
    		if(dfn[edge[i].to]==0){
    			tarjan(edge[i].to);
    			low[now]=min(low[now],low[edge[i].to]);
    		} else if(instack[edge[i].to]){
    			low[now]=min(low[now],low[edge[i].to]);
    		}
    	} 
    	if(dfn[now]==low[now]){
    		scccnt++;
    		while(dfn[s.top()]!=low[s.top()]){
    			color[s.top()]=scccnt; //标记强连通分量的颜色 
    			instack[s.top()]=false;
    			s.pop();
    		}
    		color[s.top()]=scccnt; //染色标记强连通分量
    		instack[s.top()]=false;
    		s.pop();
    	}
    }
    
posted @ 2020-07-16 00:04  BUAA-Wander  阅读(227)  评论(0编辑  收藏  举报