[新手入门][拓扑排序]

( ⊙ 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值