( ⊙ o ⊙ )!
O(∩_∩)O哈!
上个月学的拓扑排序,曾经觉得它很高深.今天就写篇简单的入门.也当是给自己复习好了~~~
咱们不用那些陌生的名词.咱们通俗点讲.
介个算法是解决甚么问题的呢?实际中,我们经常会碰到到这样一类问题:一堆元素,不一定每两个间都有严格的先后关系.先前的冒泡,快速排序等等等等都是建立在
每两个元素间都可以比较出个结果来的,举例:3, 1, 4, 1, 5排序,我们排出1,1,3,4,5的顺序,是因为他们中任意两个间都有大小关系:1<3,3<4,1<5等等.但是有这么一个
问题,7个室友在速射打游戏,午饭时间到了,他们不愿去食堂,太浪费时间了.所以他们准备每天选一个人去买翔回来.然后每周大家用1+2+3+4+5+6+7=28元速射经费奖励那个跑腿的. 作为跑腿费.周一奖励1远,以此类推...大家每人每周都得跑一次,当然就想尽量排在后面,就可以多拿点跑腿费了嘛~~~这时1~7号都有一些要求了:1号觉得他之前跑腿比5号
多,所以他起码要比5号排在后面,不然不公平! 2号觉得自己得排在1号后面, 1号觉得自己得排在3号和6号的前面....
那有没有一种安排方案让大家的要求都得到满足呢?
这个问题其实正是拓扑排序所回答的.从这里也可以知道:拓扑排序给出的排序不一定是唯一的.
拓扑排序的过程如下:
首先建图,如果A必须在B前面,就连一条A->B的有向边.同时记录每个顶点的入度(即连入该点的边的数量),出度(从该点连出的边的数量).
建图可以自由发挥.我这里给出一种方案供选用:
定义vector<int> G[MAX_V]存图,开始时清空,比如有n个顶点,那么G[i]对应的就是第i个顶点的*连出*的边所对应的顶点编号.比如如果有一条边
1->7那么我们加边时就应该这么写:G[1].push_back(7).很简单明了吧??如果边有边权(在其它图论问题中),就G[1][7] = val;(val就是1->7的边权)
入度?入度用一个数组int in[MAX_V], in[i]对应第i个顶点的入度.初始化为0,以后每加一条边u->v,就把in[v]加一
出度用数组int out[MAX_V]表示,初始化为0,每加一条边u->v就把out[u]加一.
看个实际问题:http://acm.tju.edu.cn/toj/showp.php?pid=3993
对这个问题我们怎么建图呢?我们用上面的方法试一试:
#include <cstdio>
#include <cstring> //memset
#include <iostream>
#include <vector>
using namespace std;
const int MAX_V = 128; //最多100个顶点
vector<int> G[MAX_V]; //存图用,是不是和上面一模一样?
int in[MAX_V]; //入度
int out[MAX_V]; //出度
int T; //题目的测试组数
int n; //顶点数目
int m; //边的数目
int main() {
scanf(" %d", &T); //cin >> T;
while (T--) {
scanf(" %d %d", &n, &m); //cin >> n >> m;
/* 初始化 */
memset(in, 0, sizeof(in));
memset(out, 0, sizeof(out));
for (int i = 0; i < MAX_V; ++i) { //如果你只想把自己要用的前n个清空也可以,
//但是注意编号是0~n-1还是1~n
G[i].clear(); //清空
}
int u, v; //u->v
for (int i = 0; i < m; ++i) {
scanf(" %d %d", &u, &v); //cin >> u >> v;
G[u].push_back(v); //连一条u到v的边,就往u对应的G[u]里扔进一个v
in[v]++; //有边进入v所以v的入度加一
out[u]++;//有边从u出来,所以u的出度加一
}
}
return 0;
}
以上我们就完成了建图的准备工作了.再次说明,本文是为刚接触图论的新手而设.希望不久后你会觉得这 文章真傻逼o_o
然后每次从当前剩下的入度为0的点中随便拿一个,来更新它的后继边.
试想,问题的关键就是要"满足大家的需求",那么这入度为0意味着神马?意味着没有边连向它,也就是剩下的人中没有人需要在它前面了.那我们就"满足它",
把它排到现在的新队列的后面,就当是搞定它了,然后再去满足剩下的那些人就可以了.比如4个点,有且仅一个条件是1要在2和3和4的前面.模拟一下我们
建图的过程,入度为0的点只有1,所以我们就满足它,把它排在第一个位置,然后和它关联的2,3,4因为还没排,又因为我们排序时元素都是加到原有序列末尾的,所以
加了1后,2,3,4和1就相当于没有任何关系了,因为就算加进序列那也是更晚加进去的,一定是在1后面的.所以把2,3,4和1的这个连接"割断",也就是把他们的入度减1.
减完后如果入度变为0了就加到我们的"候选名单"中,这样下一步我们就可以从这些名单中选一个继续我们无耻的排序了.
实现起来奏是:
/* top sort */
vector<int> Dong; //这就是我们的候选名单了,里面将会存进那些入度为0的点
vector<int> ans; //这里保存我们排好序的名单,每次拿出一个入度为0的点后都丢到这里来
for (int i = 1; i <= n; ++i) {
if (in[i] == 0) {
Dong.push_back(i); //如果入度为0,加进去
in[i] = -1; //标记为负数,免的影响后面
}
}
/* 因为拓扑排序是"每次拿一个入度0的点出来",所以为了把n个元素排好,就需要n次循环. */
for (int i = 1; i <= n; ++i) {
int t = Dong[Dong.size()-1]; //把最后一个取出来
Dong.pop_back(); //取出后当然要删掉去
ans.push_back(t); //先满足它,也就是排到我们最后要搞出来的排序序列中,而且是放到末尾
for (int j = 0; j < G[t].size(); ++j) {
in[G[t][j]]--; //如上所言,满足这个点后,由它连出去的那些点入度要减一
if (in[G[t][j]] == 0) {
Dong.push_back(G[t][j]); //如果减到0了,就扔进来.
in[G[t][j]] = -1; //扔进来后标记为-1,这是一个好习惯哦
}
}
}
/* 完成了 */
上面只是一个很粗糙的框架.在这里我想表达的是:其一,拓扑排序应该怎么实现,其二作为超级大水比的我们应该怎么去建图,存储中间信息,把算法用代码表达出来.开始可能很艰难.
但是当你不断地尝试这么做之后,总有一天你会发现,你已经突破这个瓶颈,你可以开始认认真真地学习算法思想本身,而不是碰到一个题就手足无措找题解模仿了.实现方法太多了,但是无论你怎么写,永远不要忘记算法的核心是它的思想,那才是灵魂!
上面的问题中,我们应该怎么判断是否排序成功了呢?拓扑排序要求每次拿出一个入度0的点来更新其它点,要做n次,那么如果n次还没拿完就已经没有入度为0的点可以拿了,我们就
说排序失败.
那怎么对付字典序最小呢?我们不是有一个待拿取的"候选列表"吗?只要我们每次从这个列表中不是随便拿,而是:总是拿编号最小的入度为0的点.就OK了~~~
int t = n+1, index;
for (int j = 0; j < Dong.size(); ++j) {
if (Dong[j] < t) {
t = Dong[j];
index = j;
}
}
Dong.erase(Dong.begin()+index,1);
/* 拿t去更新就好了 */
也可以加一个数组bool vis[MAX_V].用过的点或者入度不为0的点,我们就标记为true.然后只需要:
int t;
for (int ii = 1; ii <= n; ++ii) {
if (!vis[ii]) {
t = ii;
break;
}
}
for (int j = 0; j < G[t].size(); ++j) {
.../
if (in[G[t][j]] == 0) {
vis[G[t][j]] = false;
in[G[t][j]] = -1;
}
}
都是很随便的,怎么写都行.俺刚AC了一下,上个月还WA无数次的题...现在觉得好简单...
拓扑排序部分我的实现:
queue<int> ans; //因为要求字典序最小,我们就用队列,先进先出嘛~~~
//候选列表,但是用优先队列保存,每次拿到的就定是序号最小的点了
priority_queue<int, vector<int>, greater<int> > Q;
for (int i = 1; i <= n; ++i) {
if (in[i] == 0) {
Q.push(i);
in[i] = -1;
}
}
bool ok = true;//标记是否成功,我们知道,这里唯一不成功的可能就
//是:某次我们想拿入度为0的点拿不到了~~~
for (int i = 1; i <= n; ++i) {
if (Q.empty()) { //n次循环还没睾丸,就已经没有入度为0的点剩余了,失败
ok = false;
break;
}
int t = Q.top(); Q.pop();
ans.push(t);
for (int j = 0; j < G[t].size(); ++j) {
--in[G[t][j]];
if (in[G[t][j]] == 0) {
Q.push(G[t][j]);
in[G[t][j]] = -1;
}
}
}
这题数据不大,所以无所谓.但是如果数据大了,这里的优先队列priority_queue会非常有用,当然,为了字典序,同样的可以用set来保存.STL的set内部是红黑树,性能非常优秀! priority_queue是堆实现,也很棒.插入都是O(logn)的,然后我们每次都只取编号最小的元素,所以查询是O(1)的,非常高效^_^
有问题可以给俺留言哦~~~
俺的邮箱763400483@qq.com