【2023武汉集训模拟赛7】灭国 做题记录

有个 nn 个点 mm 条边的 DAG,你要加不超过 kk 条边,使得加完之后还是一个 DAG,并且 这个 DAG 的字典序最小拓扑序字典序最大。

1n,m,k1051\le n,m,k\le 10^5

求出给定 DAG 的字典序最小拓扑序显然只需要把拓扑排序的队列换成小根堆,那么考虑贪心。

称原图拓扑排序所用的小根堆为 qq,那么某一时刻若 qq 中有 >1>1 个点且加边的次数还没用完则可以通过加边来延后 qq 中最小的点被删除的时间。设这个最小的点是 uu,那么把 uuqq 中删除,加入到一个大根堆 q2q2 中。注意到这样操作相当于是加了一条指向 uu 的边。

qq 中没有元素且 q2q2 中还有元素时,为了保证图还是 DAG 我们就不得不让 q2q2 中的某个点被删掉了,显然把 q2q2 中最大的点删掉是最优的。设这个点是 uu,那么指向 uu 的边一定是由当前拓扑序中最后一个点 vv 连过来的,那么把边 vuv\to u 加入答案序列中即可。

最后有一个细节,第一层一定要留一个点在 qq 中,因为无法加边。以后的每一层,qq 中还剩下一个点 uu 时,若 q2q2 也非空,且最大的点为 vv,并且还能加边,那么:

  • u>vu>v 此时延后 uu 被删除的时间是没有意义的,因为加入 q2q2 后它马上就会从 q2q2 中取出,所以直接顺其自然;
  • u<vu<v 此时延后 uu 被删除的时间显然是更优的,那么把 uuqq 中删除,加入 q2q2

代码如下:

#include <iostream>
#include <cstdio>
#include <set>
#include <vector>
#include <queue>

using namespace std;

const int S=100005;

int n,m,k;
set<pair<int,int>> st;
vector<int> g[S];
int ind[S];
int cnt,a[S];
vector<pair<int,int>> ans;

int main()
{
	freopen("destroy.in","r",stdin);
	freopen("destroy.out","w",stdout);
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		if(st.count(make_pair(x,y))) continue;
		st.insert(make_pair(x,y));
		g[x].push_back(y);
		ind[y]++;
	}
	priority_queue<int> q,q2;
	for(int i=1;i<=n;i++) if(ind[i]==0) q.push(-i);
	while(k>0&&q.size()>1) q2.push(-q.top()),q.pop(),k--;
	while(!q.empty()||!q2.empty())
	{
		int u;
		if(!q.empty()) u=-q.top(),q.pop();
		else
		{
			u=q2.top();
			q2.pop();
			ans.push_back(make_pair(a[cnt],u));
		}
		a[++cnt]=u;
		for(int v:g[u])
		{
			if(--ind[v]==0) q.push(-v);
		}
		while(k>0&&q.size()>1) q2.push(-q.top()),q.pop(),k--;
		if(k>0&&!q.empty()&&!q2.empty()&&-q.top()<q2.top())
		{
			q2.push(-q.top()),q.pop();
			k--;
		}
	}
	for(int i=1;i<=n;i++) printf("%d ",a[i]);
	printf("\n");
	printf("%d\n",ans.size());
	for(pair<int,int> u:ans)
	{
		printf("%d %d\n",u.first,u.second);
	}
	return 0;
}