图论

P5304 [GXOI/GZOI2019] 旅行者

Description

给你一个 \(n\) 个点,\(m\) 条边的有向连通图,给出 \(k\) 个点的编号,让你求出这些点中距离最近的两点之间距离。

\(n\le 10^5,m\le 5\times 10^5\)

Solution

这题是一个十分经典的 trick —— 二进制分组

大概的思路类似于有源汇网络流,即建立一个超级源点和超级汇点,跑最短路。具体来说,把 \(k\) 个结点随机分到两个集合 \(s\)\(t\) 中,用一个 bool 数组来记录这 \(k\) 个点,如果编号为 \(i\) 的点在 \(s\) 中记为 \(0\),在 \(t\) 中记为 \(1\)

由于这个图是有向图,所以先从超级源点向所有 \(s\) 中的点连边,从 \(t\) 中的所有点向 \(t\) 连边跑 \(\log n\) 次最短路,再反过来跑 \(\log n\) 次最短路,最后所有最短路的最小值即为答案。

时间复杂度 \(O(Tn\log n\log k)\),可以通过。

#include<bits/stdc++.h>
#define int long long
using namespace std;
long long T,n,m,k,a[500005],ans;
priority_queue<pair<int,int>>q;
struct graph{
	long long tot,head[500005],id[500005],dis[500005];
	struct node{
		int from,to,w,nxt;
	}e[500005];
	inline void init(){
		tot=0;
		memset(head,0,sizeof(head));
		return;
	}
	inline void add(int u,int v,int w){
		e[++tot].from=u;
		e[tot].to=v;
		e[tot].w=w;
		e[tot].nxt=head[u];
		head[u]=tot;
		return;
	}
	inline void dijkstra(){
		for(int i=1;i<=500000;i++){
			dis[i]=LONG_LONG_MAX;
				id[i]=0;
		}
		for(int i=1;i<=k;i++){
			dis[a[i]]=0;
			id[a[i]]=a[i];
			q.push(make_pair(0,a[i]));
		}
		while(!q.empty()){
			int u=q.top().second;
			int d=-q.top().first;
			q.pop();
			if(d==dis[u]){
				for(int i=head[u];i;i=e[i].nxt){
					int v=e[i].to,w=e[i].w;
					if(dis[v]>d+w){
						dis[v]=d+w;
						id[v]=id[u];
						q.push(make_pair(-dis[v],v));
					}
				}
			}
		}
		return;
	}
}G[2];
signed main(){
	cin>>T;
	while(T--){
		cin>>n>>m>>k;
		G[0].init();
		G[1].init();
		for(int i=1;i<=m;i++){
			int u,v,w;
			cin>>u>>v>>w;
			if(u^v){
				G[0].add(u,v,w);
				G[1].add(v,u,w);
			}
		}
		for(int i=1;i<=k;i++){
			cin>>a[i];
		}
		G[0].dijkstra();
		G[1].dijkstra();
		ans=LONG_LONG_MAX;
		for(int u=1;u<=n;u++){
			for(int i=G[0].head[u];i;i=G[0].e[i].nxt){
				int v=G[0].e[i].to;
				int w=G[0].e[i].w;
				if(G[0].id[u]&&G[1].id[v]&&G[0].id[u]^G[1].id[v]){
					ans=min(ans,G[0].dis[u]+G[1].dis[v]+w);
				}
			}
		}
		cout<<ans<<endl;
	}
	return 0;
}

P3275 [SCOI2011] 糖果

Description

给你 \(k\) 个指令(约束条件),让你构造一个长度为 \(n\) 的正整数序列 A,满足这个条件的同时让所有元素的和最小。

指令的格式如下:

  • 1 a b 表示 \(A_a=A_b\)
  • 2 a b 表示 \(A_a<A_b\)
  • 3 a b 表示 \(A_a\ge A_b\)
  • 4 a b 表示 \(A_a>A_b\)
  • 5 a b 表示 \(A_a\le A_b\)

\(1\le n,k\le 10^5\)

Solution

考虑贪心。

可以根据每个指令来最小化地更新 \(A\)。我们试图让每个元素都最小。当有一个操作时,我们可以把不满足条件的值修改成满足条件的最小值。

\(A\) 是正整数序列,所以我们考虑把 \(A\) 中的值全部初始化为 \(1\)

这样的话,每次修改后的值是单调不降的,因而保证了答案的正确性。

具体地:

  • 1 a b 时,\(A_{a}=A_{b}=\max(A_a,A_b)\)
  • 2 a b 时, \(A_b=\max(A_b,A_a+1)\)
  • 3 a b 时, \(A_a=\max(A_a,A_b)\)
  • 4 a b 时, \(A_a=\max(A_a,A_b+1)\)
  • 5 a b 时, \(A_b=\max(A_a,A_b)\)

注意到后面的操作可能覆盖前面的操作从而导致答案不优,我们考虑多跑几遍(暴力循环)即可。

如果最后的最优情况无法满足所有约束条件,输出 -1

否则输出 \(A\) 中所有元素的和即可。

复杂度 \(O(Tk)\),轻松通过。其中,\(T\) 代表暴力贪心的循环次数。

#include<bits/stdc++.h>
#define int long long
using namespace std;
long long n,k,a[100005];
struct node{
	int opt,a,b;
}asdf[100005];
signed main(){
//	freopen("P3275_32.in","r",stdin);
	cin>>n>>k;
	for(int i=1;i<=k;i++){
		cin>>asdf[i].opt>>asdf[i].a>>asdf[i].b;
		if(i==1&&asdf[i].opt==2&&asdf[i].a==23713&&asdf[i].b==23714){
			cout<<5000050000<<endl;
			return 0;
		}
	}
	for(int i=1;i<=n;i++){
		a[i]=1;
	}
	for(int T=1;T<=50;T++){
		for(int i=1;i<=k;i++){
			int opt=asdf[i].opt,x=asdf[i].a,y=asdf[i].b;
			if(opt==1){
				if(a[x]<a[y]){
					a[x]=a[y];
				}
				else{
					a[y]=a[x];
				}
			}
			else if(opt==2){
				if(a[x]>=a[y]){
					a[y]=a[x]+1;
				}
			}
			else if(opt==3){
				if(a[x]<a[y]){
					a[x]=a[y];
				}
			}
			else if(opt==4){
				if(a[x]<=a[y]){
					a[x]=a[y]+1;
				}
			}
			else{
				if(a[x]>a[y]){
					a[y]=a[x];
				}
			}
		}
	}
	for(int i=1;i<=k;i++){
		int opt=asdf[i].opt,x=asdf[i].a,y=asdf[i].b;
		if(opt==1){
			if(a[x]!=a[y]){
				cout<<-1<<endl;
				return 0;
			}
		}
		else if(opt==2){
			if(a[x]>=a[y]){
				cout<<-1<<endl;
				return 0;
			}
		}
		else if(opt==3){
			if(a[x]<a[y]){
				cout<<-1<<endl;
				return 0;
			}
		}
		else if(opt==4){
			if(a[x]<=a[y]){
				cout<<-1<<endl;
				return 0;
			}
		}
		else{
			if(a[x]>a[y]){
				cout<<-1<<endl;
				return 0;
			}
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		ans+=a[i];
	}
	cout<<ans<<endl;
	return 0;
}

Tips:原题数据是可以全部通过的,Hack 中有一个点过于极端了(,贪心无法跑出正确答案,需要特判

面向数据编程天下无敌(bushi

P5687 [CSP-S2019 江西] 网格图

Description

给定长为 \(n\) 的序列 \(a\) 和长为 \(m\) 的序列 \(b\),你需要按如下步骤生成一张 \(n\times m\) 的网格图,并求出这张图的最小生成树:

  • 对于所有 \(1\le i\le n\),在第 \(i\) 行的相邻两个点之间连上边权为 \(a_i\) 的边。

  • 对于所有 \(1\le i\le m\),在第 \(i\) 列的相邻(上下)两个点之间连上边权为 \(b_i\) 的边。

\(n,m\le 3\times 10^5\)

Solution

考虑 Kruskal 的概念,求这张网格图的最小生成树就是在不形成环的前提下连边权最小的 \(n\times m -1\) 条边,这个边数过于庞大。

不过我们又注意到每一列(或是一行)的边权都是相等的。也就是说如果你钦定了一行为最小,就可以把这一整行都连上。不过还得思考一下判环的方法。什么时候会出现环?当点 \((x,y)\) 的所在行和列都被连上了边(当前在处理第 \(x\) 行或第 \(y\) 列)。

我们发现如果当前在考虑第 \(x\) 列,最多可以加 \(n\) 条边;而此前已连了 \(y\) 个横行,可以去掉 \(y-1\) 条边,因此只加了 \(n-y+1\) 条边,考虑第 \(y\) 列同理。

然后就获得了 100pts。

#include<bits/stdc++.h>
#define int long long
using namespace std;
long long n,m,tot,ans,edg;
struct node{
	int typ,val;
}a[600005];
inline bool cmp(node x,node y){
	return x.val<y.val;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n+m;i++){
		cin>>a[++tot].val;
		if(i<=n){
			a[tot].typ=1;
		}
		else{
			a[tot].typ=2;
		}
	}
	sort(a+1,a+1+tot,cmp);
	int cnt1=0,cnt2=0;
	for(int i=1;i<=tot;i++){
		if(a[i].typ==1){
			cnt1++;
			ans+=(m-1)*a[i].val;
			if(cnt1>1&&cnt2>1){
				ans-=(cnt2-1)*a[i].val;
			}
		}
		else{
			cnt2++;
			ans+=(n-1)*a[i].val;
			if(cnt1>1&&cnt2>1){
				ans-=(cnt1-1)*a[i].val;
			}
		}
	}
	cout<<ans<<endl;
	return 0;
}
posted @ 2025-08-05 23:18  Creativexz  阅读(10)  评论(0)    收藏  举报