5.19~5.25

拓扑排序

拓扑排序不是对数字进行大小排序的那种排序,而是对一系列事物的顺序关系和依赖关系进行排序,属于图论。拓扑排序的排序结果通常不是唯一的。

一个图能进行拓扑排序的充要条件是它是一个**有向无环图(DAG) **。

拓扑排序需要用到点的入度(Indegree)和出度 (Outdegree)

  • 入度:以点$ v
    \(为终点的边的数量,称为\) v $的入度。
  • 出度:以点$ u \(为起点的边的数量,称为\) u $的出度。

一个点的入度为0,说明它是起点,是排在最前面的;一个点的出度为0,说明它是终点,排在最后面。

拓扑排序的简单的图遍历,用BFS和DFS都能实现。

基于BFS的拓扑排序

(1)无前驱的顶点优先

先输出入度为0的点(无前驱,优先级最高),以下是具体步骤:

  1. 找所有入度为0的点,放入队列作为起点,这些点谁先谁后没有关系。如果找不到入度为0的点,说明这个图不是DAG,不存在拓扑排序。
  2. 弹出队首元素a,将a的所有邻居点入度-1,入度减为0的邻居点入队。没有减为0的不入队。
  3. 继续上述操作,直到队列为空为止。队列的输出就是一个拓扑排序。

拓扑排序无解的判断(找环):

如果队列已空,但是还有点未入队,那么这些点的入度都不为0,说明图不是DAG,不存在拓扑排序。这也说明图上存在环,可以用来找环,没有进入队列的点,就是环上的点。

(2)无后驱的顶点优先

和无前驱反过来就行,先找出度为0的点。

BFS的时间复杂度为$ O(n+m) $,其中m为入度为0的点。

基于DFS的拓扑排序

DFS很适合拓扑排序。

一个有向无环图,从一个入度为0的点$ u \(开始DFS,DFS递归返回的顺序就是拓扑排序(是一个**逆序**)。DFS递归返回的首先是最底层的点,它一定是出度为0的点;然后逐步回退,最后输出起点\) u $。

如果图是有环图,则不存在拓扑排序,那么在递归时,会出现回退边。在代码中这样发现回退边:记录每个点的状态,如果dfs递归到某个点时发现它仍在前面的递归中没有处理完毕,说明存在回退边,不存在拓扑排序。

例题POJ 1270

#include <bits/stdc++.h>
using namespace std;
#define check (s[i]>='a' && s[i]<='z')
const int maxn = 30;

int n,in[maxn],a[maxn],topo[maxn];
bool link[maxn][maxn],vis[maxn];

void dfs(int u,int cnt){
	topo[cnt] = u;
	if(cnt == n-1){
		for(int i=0;i<n;i++) cout << char('a'+topo[i]) << ' ';
		cout << '\n';
		return;
	}
	vis[u] = 1;
	for(int i=0;i<n;i++){
		if(!vis[a[i]] && link[u][a[i]])
			in[a[i]]--;
	}
	for(int i=0;i<n;i++){
		if(!vis[a[i]] && !in[a[i]])
			dfs(a[i],cnt+1);
	}
	for(int i=0;i<n;i++){
		if(!vis[a[i]] && link[u][a[i]])
			in[a[i]]++;
	}
	vis[u] = 0;
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	string s;
	while(getline(cin,s)){
		memset(in,0,sizeof(in));
		memset(link,0,sizeof(link));
		memset(vis,0,sizeof(vis));
		int len = s.size();
		n = 0;
		for(int i=0;i<len;i++)
			if(check) a[n++] = s[i]-'a';
		sort(a,a+n);
		getline(cin,s);
		len = s.size();
		int f = 1,st,ed;
		for(int i=0;i<len;i++){
			if(f && check){
				f = 0;
				st = s[i] - 'a';
				continue;
			}
			if(!f && check){
				f = 1;
				ed = s[i] - 'a';
				link[st][ed] = 1;
				in[ed]++;
				continue;
			}
		}
		for(int i=0;i<n;i++){
			if(!in[a[i]])
				dfs(a[i],0);
		}
		cout << '\n';
	}
	return 0;
}

洛谷P1113

这题难度不高,我dfs和bfs各写了一份

bfs

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4+5;

int n,dp[maxn],in[maxn],a[maxn];
vector <int> e[maxn];

void bfs(){
	queue <int> q;
	for(int i=1;i<=n;i++){
		if(!in[i]){
			q.push(i);
			dp[i] = a[i];
		}
	}
	while(!q.empty()){
		int b = q.front();
		q.pop();
		for(auto i:e[b]){
			in[i]--;
			if(!in[i]) q.push(i);
			dp[i] = max(dp[i],dp[b]+a[i]);
		}
	}
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n;
	for(int i=0;i<n;i++){
		int a1,len,c;
		cin >> a1 >> len; 
		a[a1] = len;
		do{
			cin >> c;
			e[a1].push_back(c);
			in[c]++;
		}while(c);
	}
	bfs();
	int ans = 0;
	for(int i=1;i<=n;i++){
		ans = max(ans,dp[i]);
	}
	cout << ans << '\n';
	return 0;
}

dfs

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4+5;

int n,a[maxn],dp[maxn];
vector <int> e[maxn];

int dfs(int x){
	if(dp[x]) return dp[x];
	int add = 0;
	for(int i=0;i<e[x].size();i++){
		add = max(add,dfs(e[x][i]));
	}
	dp[x] += add + a[x];
	return dp[x];
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n;
	for(int i=0;i<n;i++){
		int a1,len,c;
		cin >> a1 >> len;
		a[a1] = len;
		do{
			cin >> c;
			e[c].push_back(a1);
		}while(c);
		
	}
	int ans = 0;
	for(int i=1;i<=n;i++){
		ans = max(ans,dfs(i));
	}
	cout << ans << '\n';
	return 0;
}

洛谷P1347

虽一眼拓扑排序,但是这题有点难写,被各种坑卡了一下午,考验代码细节。

选择BFS进行拓扑

#include <bits/stdc++.h>
using namespace std;
const int maxn = 605;
int ji = 1;
int n,m,in[30],ou[30],cnt = 0;
vector <int> e[maxn];
map <int,int> ma;
bool vis[30]={0},c2 = 0,c3 = 0;

int topo(){
	int t = 0;
	bool temp[30]={0};
	queue <int> q;
	memset(ou,0,sizeof(ou));
	cnt = 0;
	//cout << "times: " << ji << " in[C]: " << in[3] << '\n';ji++;
	for(int i=1;i<=n;i++){
		if(!in[i] && vis[i]){
			//cout << "in[i] de i: " << i << " in[i]: " << in[i] << '\n';
			q.push(i);
			t++;
			ou[cnt++] = i, temp[i] = 1;
			//cout << "rukou:" << i << '\n';
		}
	}
	if(t != 1 && t != 0) c3 = 1;
	//cout << "t1: " << t << '\n';
	if(q.empty()){
		//cout << "111\n";
		c2 = 1;
		return 2;
	}
	while(!q.empty()){
		int a = q.front();
		q.pop();
		t = 0;
		for(auto i:e[a]){
			//if(temp[i]) {cout<<"a:" << a << " i:" << i << " temp[i]:" << temp[i];c2 = 1; return 2;}
			in[i]--;
			ma[i]++;
			if(!in[i]){
				q.push(i);
				t++;
				ou[cnt++] = i, temp[i] = 1;
			}
		}
		if(t != 1 && t != 0) c3 = 1;
		//cout << "t2: " << t << '\n';
	}
	for(int i=1;i<=26;i++) if(in[i]) {c2 = 1; return 2;}
	for(auto i:ma) in[i.first] += i.second;
	ma.clear();
	//cout << "times: " << ji++ << " c2: " << c2 << " c3: " << c3 << '\n';
	if(cnt == n && !c2 && !c3) return 1;
	return 0;
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n >> m;
	for(int i=1;i<=m;i++){
		string s; cin >> s;
		int s1 = s[0]-'A'+1, s2 = s[2]-'A'+1;
		vis[s1] = vis[s2] = 1;
		in[s2]++;
		e[s1].push_back(s2);
		int pd = topo();
		if(pd == 1){
			cout << "Sorted sequence determined after " << i << " relations: ";
			for(int i=0;i<cnt;i++) cout <<char(ou[i]+'A'-1);
			cout << ".\n";
			return 0;
		}
		if(pd == 2){
			cout << "Inconsistency found after " << i << " relations.\n";
			return 0;
		}
	}
	cout << "Sorted sequence cannot be determined.\n";
	return 0;
}

cf 1019 div.2

作为中国人,题面放中文

A

很明显,数组y可以用数组a的所有元素的最小公倍数_LCM_构造出来

而当a中有重复的元素时,就不能构造出y了

所以对a去重即可

#include <bits/stdc++.h>
using namespace std;

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int t; cin >> t;
	while(t--){
		bool vis[101] = {0};
		int n; cin >> n;
		int len = n;
		for(int i=1;i<=n;i++){
			int c; cin >> c;
			if(!vis[c]) vis[c] = 1;
			else len--;
		}
		cout << len << '\n';
	}
	return 0;
}

B

注意到(注意力惊人),反转一个字串,最多可以减少两次操作次数,

像“0101””101“这种,既有”01“,也有”10“,一个”1“两边都是0,那么0->1->0就要切换两次;一个”0“两边都是1,那么1->0->1同样要切换两次;

如果1作为开头,还要再切换一次

找规律可以发现,有三种结果

当切换次数>2时,一定可以触发减少2次的情况,所以结果为n+cnt-2

当切换次数==2时,可以触发减少1次的情况,所以结果为n+1

当切换次数<2时,要么全0,要么就减少不了,所以是n+cnt

#include <bits/stdc++.h>
using namespace std;

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int t; cin >> t;
	while(t--){
		int n; cin >> n;
		string s; cin >> s;
		int cnt = 0;
		if(s[0] == '1') cnt++;
		for(int i=1;i<n;i++)
			if(s[i] != s[i-1]) cnt++;
		if(cnt > 2) cout << n+cnt-2 << '\n';
		else if(cnt == 2) cout << n+1 << '\n';
		else cout << n+cnt << '\n';
	}
	return 0;
}

C

思维题

把一个数列划分为三段,要求三段的每一段的中位数所构成的数列的中位数 <= k

中位数的取法是$ floor(\frac{m}{2}) $,m是数列长度

做一个条件前、后缀和,触发累加的条件是a[i]<=k

条件前缀和是pre[i],存放[1,i]<=k的数字个数

条件后缀和是hou[i](英语不好不知道该用啥单词),存放[n-i+1,n]<=k的数字个数

而切割数列的切割点有三种情况:

  • 两个左切割点(由前缀和得出)
  • 两个右切割点(由后缀和得出)
  • 一左一右(要求:l < r-1)

左切割点的得出方法是2*pre[i] >= i,右切割点的得出方法是2*hou[i] >= n-i+1

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+5;

int a[maxn],pre[maxn],hou[maxn];

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int t; cin >> t;
	while(t--){
		memset(pre,0,sizeof(pre));
		memset(hou,0,sizeof(hou));
		int n,k,l=0,r=0; cin >> n >> k;
		for(int i=1;i<=n;i++){
			cin >> a[i];
			pre[i] += pre[i-1] + (a[i] <= k);
		}
		for(int i=n;i>=1;i--) hou[i] += hou[i+1] + (a[i] <= k);
		bool pd = 0;
		for(int i=1;i<=n;i++){
			if(l && 2*pre[i] >= i && pre[i] > 1) pd = 1;
			if(!l && 2*pre[i] >= i) l = i;
		}
		for(int i=n;i>=1;i--){
			if(r && 2*hou[i] >= n-i+1 && hou[i] > 1) pd = 1;
			if(!r && 2*hou[i] >= n-i+1) r = i;
		}
		if(l && r && l < r-1) pd = 1;
		cout << (pd ? "YES\n" : "NO\n");
		//for(int i=0;i<R.size();i++) cout << " R[i]: "<< R[i];
		//cout << '\n';
		//for(int i=1;i<=n;i++) cout << hou[i] << ' ';
		//cout << '\n';
		//if(L.size() >= 2 || R.size() >= 2) cout << "YES\n";
		
	}
	return 0;
}

D

看着论文一样的题目,瓜坐了很久...

挺难的构造题

主要用双指针,分奇偶轮数来构造这个排列。

  • 奇数轮次:删除非局部最小值。为了确保被删除的元素不是局部最小值,它们应该比相邻的元素大。因此这些元素的值应该较大或较小。然后这样操作:从左到右处理 <font style="color:rgb(0, 0, 0);">rou[i]</font> 中索引小于 <font style="color:rgb(0, 0, 0);">index</font> 的元素,分配较大的值(<font style="color:rgb(0, 0, 0);">r</font> 递减),再反转 <font style="color:rgb(0, 0, 0);">rou[i]</font>,从右到左处理索引大于 <font style="color:rgb(0, 0, 0);">index</font> 的元素,分配较大的值。
  • 偶数轮次:删除非局部最大值。为了确保被删除的元素不是局部最大值,它们应该比相邻的元素小。因此这些元素的值应该较小或较大。相应的就是:从左到右处理 <font style="color:rgb(0, 0, 0);">rou[i]</font> 中索引小于 <font style="color:rgb(0, 0, 0);">index</font> 的元素,分配较小的值(<font style="color:rgb(0, 0, 0);">l</font> 递增)。接着反转 <font style="color:rgb(0, 0, 0);">v[i]</font>,从右到左处理索引大于 <font style="color:rgb(0, 0, 0);">vv</font> 的元素,分配较小的值。

两个指针lr,初始化为1和n。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+5;

int t,n,a[maxn],ans[maxn];
vector <int> rou[maxn];

int times(int m){
	int x = 1;
	while(m){
		x++;
		m >>= 1;
	}
	return x;
}

void solve(){
	memset(a,0,sizeof(a));
	memset(ans,0,sizeof(ans));
	int n,index; cin >> n;
	for(int i=1;i<=n;i++){
		cin >> a[i];
		rou[i].clear();
	}
	for(int i=1;i<=n;i++){
		if(a[i] == -1) index = i;
		else rou[a[i]].push_back(i);
	}
	int k = times(n), l = 1, r = n;
	for(int i=1;i<=k;i++){
		if(i & 1){
			for(auto j:rou[i]){
				if(j < index) ans[j] = r--;
				else break;
			}
			reverse(rou[i].begin(),rou[i].end());
			for(auto j:rou[i]){
				if(j > index) ans[j] = r--;
				else break;
			}
		}
		else{
			for(auto j:rou[i]){
				if(j < index) ans[j] = l++;
				else break;
			}
			reverse(rou[i].begin(),rou[i].end());
			for(auto j:rou[i]){
				if(j > index) ans[j] = l++;
				else break;
			}
		}
	}
	ans[index] = l;
	for(int i=1;i<=n;i++) cout << ans[i] << ' ';
	cout << '\n';
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> t;
	while(t--){
		solve();
	}
	return 0;
}
posted @ 2025-06-14 12:29  HLAIA  阅读(7)  评论(0)    收藏  举报