纸上谈兵: 拓扑排序

简介: 作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!   《文明》是一款风靡20多年的回合制策略游戏,由Sid Meier开发。《文明》结构宏大,内容丰富,玩法多样,游戏性强,称得上是历史上最伟大的游戏。

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

《文明》是一款风靡20多年的回合制策略游戏,由Sid Meier开发。《文明》结构宏大,内容丰富,玩法多样,游戏性强,称得上是历史上最伟大的游戏。在文明中,你可以选择某个文明的,从部落开始发展,在接下来的几千年的历史中,发展科技、开荒拓野、发动战争等等。游戏在保持自由度的前提下,对各个社会文明的发展顺序有很好的仿真性,让玩家仿佛置身于历史长河,坐观文明的起落。美国的一些大学的历史系甚至于使用该游戏作为教学工具。

(作为《文明》的忠实爱好者,多少个昼夜耗费在一张张地图上啊。好吧,是为了学习历史。)

 

“科技树”

《文明》中的科技树

游戏有一个“科技树”系统,即你可以按照该图所示的顺序来发展科技。“科技树”是这样一个概念: 为了研发某个科技,我们必须已经掌握了该科技的所有前提科技(prerequisite)。科技树中有一些箭头,从A科技指向B科技,那么A就是B的前提科技。比如,根据上面的科技树,为了研发物理(Physics),我们必须已经掌握了化学(Chemistry)和天文学(Astronomy)。而为了研发化学(Chemistry),我们又必须掌握了火药(Gunpowder)。如果一个科技没有其它科技指向,那么就不需要任何前提科技就可以研发,比如图中的封建主义(Feudalism)。如果同一时间只能研发一项科技,那么玩家的科技研发就是一个序列,比如封建主义,工程(Engineering),发明(Invention),火药,一神教(monotheism),骑士制度(Chivalry)…… 有些序列是不合法的,比如工程,发明,火药……,在研发的“发明”时,并没有满足“封建主义”的前提条件。

 

一个有趣的问题是,如何找到合法的序列呢?当我们在设计计算机玩家时,很可能需要解决这样一个问题。

图(graph)中的拓扑排序算法(Topological Sort)可以给出一个合法序列。虽然在游戏中被称为“科技树”,但“科技树”并不符合数据结构中的树结构。在数据结构中,树的每个节点只能由一个父节点,整个树只有一个根节点。因此,“科技树”是一个不折不扣的图结构。此外,该树是一个有向的无环(acyclic)图。图中不存在环 (cycle, 环是一个长度大于0的路径,其两端为同一节点)。

 

拓扑排序

拓扑排序利用了一个事实,即在一个无环网络中,有一些节点是没有箭头指入的,比如科技树中的一神教、封建主义、工程。它们可以作为序列的起点。

  • 选择没有箭头指入的节点中的任一个,可以作为合法序列的起点,放入序列。
  • 当我们将某个节点放入序列后,去掉该节点以及从该节点指出的所有箭头。在新的图中,选择没有箭头指向的节点,作为序列中的下一个点。
  • 重复上面的过程,直到所有的节点都被放入序列。

 

对于每个节点,我们可以使用一个量入度(indegree),用来表示有多少个箭头指入该节点。当某个节点被删除时,图发生变化,我们需要更新图中节点的入度。

为了方便,我将“科技树”中的节点编号,为了符合C语言中的传统,编号从0开始。下面是对应关系:

0 1 2 3 4 5 6 7 8 9
一神教 神学 印刷术 民主 自由艺术 骑士制度 音乐理论 银行  经济学 封建主义
10 11 12 13 14 15 16 17 18 19
教育 天文学 航海术 物理  重力理论 发明 火药 化学 磁学 工程
20 21                
冶金 军事传统                

 

我们根据编号,绘制上述图(graph)。我同时用三种颜色,来表示不同点的入度(indegree):

我们使用邻接表的数据结构(参考纸上谈兵: 图),来实现图。构建代码如下:

/* By Vamei */
#include <stdio.h>
#include <stdlib.h>

#define NUM_V 22

typedef struct node *position;

/* node */
struct node {
    int element;
    position next;
};

/* 
 * operations (stereotype)
 */
void insert_edge(position, int, int);
void print_graph(position graph, int nv);

/* for testing purpose */
void main()
{
    struct node graph[NUM_V];
    int i;

    // initialize the vertices
    for(i=0; i<NUM_V; i++) {
        (graph+i)->element = i;
        (graph+i)->next    = NULL;
    }

    // insert edges
    insert_edge(graph,0,1);
    insert_edge(graph,0,5);
    insert_edge(graph,1,2);
    insert_edge(graph,1,10);
    insert_edge(graph,2,3);
    insert_edge(graph,3,4);
    insert_edge(graph,7,8);
    insert_edge(graph,9,5);
    insert_edge(graph,9,15);
    insert_edge(graph,10,6);
    insert_edge(graph,10,7);
    insert_edge(graph,10,11);
    insert_edge(graph,11,12);
    insert_edge(graph,11,13);
    insert_edge(graph,13,14);
    insert_edge(graph,13,18);
    insert_edge(graph,15,16);
    insert_edge(graph,16,17);
    insert_edge(graph,17,13);
    insert_edge(graph,17,20);
    insert_edge(graph,19,15);
    insert_edge(graph,20,21);

    print_graph(graph,NUM_V);
}

/* print the graph */
void print_graph(position graph, int nv) {
    int i;
    position p;
    for(i=0; i<nv; i++) {
        p = (graph + i)->next;
        printf("From %3d: ", i);
        while(p != NULL) {
            printf("%d->%d; ", i, p->element);
            p = p->next;
        }
        printf("\n");
    }
}

/*
 * insert an edge
 */
void insert_edge(position graph,int from, int to)
{
    position np;
    position nodeAddr;

    np = graph + from;

    nodeAddr = (position) malloc(sizeof(struct node));
    nodeAddr->element = to;
    nodeAddr->next    = np->next;
    np->next = nodeAddr;
}

 

我们可以根据上面建立的图,来获得各个节点的入度。使用一个数组indeg来储存各个节点的入度,即indeg[i] = indgree of ith node。下面的init_indeg()用于初始化各节点的入度:

// according to the graph, initialize the indegree at each vertice
void init_indeg(position graph, int nv, int indeg[]) {
    int i;
    position p;

    // initialize
    for(i=0; i<nv; i++) {
        indeg[i] = 0;
    }

    // assimilate the graph
    for(i=0; i<nv; i++) {
        p = (graph + i)->next;
        while(p != NULL) {
            (indeg[p->element])++;
            p = p->next;
        }
    }   
}

 

正如我们之前叙述的,我们需要找到入度为0的节点,并将这些节点放入序列。find_next()就是用于寻找下一个入度为0的节点。找到对应节点后,返回该节点,并将该节点的入度更新为-1:

/* find the vertice with 0 indegree*/
int find_next(int indeg[], int nv) {
    int next;
    int i;
    for(i=0; i<nv; i++) {
        if(indeg[i] == 0) break;
    }
    indeg[i] = -1;

    return i;
}

 

当我们取出一个节点放入序列时,从该节点出发,指向的节点的入度会减1,我们用update_indeg()表示该操作:

// update indeg when ver is removed
void update_indeg(position graph, int nv, int indeg[], int ver) {
    position p;
    p = (graph + ver)->next;
    while(p != NULL) {
        (indeg[p->element])--;
        p = p->next;
    }
}

 

有了上面的准备,我们可以寻找序列。使用数组seq来记录序列中的节点。下面的get_seq()用于获得拓扑排序序列:

// return the sequence
void get_seq(position graph, int nv, int indeg[], int seq[]){
    int i;
    int ver;
    for(i = 0; i<nv; i++) {
        ver = find_next(indeg, nv);
        seq[i] = ver;
        update_indeg(graph, nv, indeg, ver);
    }
}

 


综合上面的叙述,我们可以使用下面代码,来实现拓扑排序:

/* By Vamei */
#include <stdio.h>
#include <stdlib.h>

#define NUM_V 22

typedef struct node *position;

/* node */
struct node {
    int element;
    position next;
};

/* 
 * operations (stereotype)
 */
void insert_edge(position, int, int);
void print_graph(position, int);
void init_indeg(position, int , int *);
void update_indeg(position, int, int *, int);
void get_seq(position, int, int *, int *);
int find_next(int *, int);

/* for testing purpose */
void main()
{
    struct node graph[NUM_V];
    int indeg[NUM_V];
    int seq[NUM_V];
    int i;
    
    // initialize the graph
    for(i=0; i<NUM_V; i++) {
        (graph+i)->element = i; 
        (graph+i)->next    = NULL;
    }

    
    insert_edge(graph,0,1);
    insert_edge(graph,0,5);
    insert_edge(graph,1,2);
    insert_edge(graph,1,10);
    insert_edge(graph,2,3);
    insert_edge(graph,3,4);
    insert_edge(graph,7,8);
    insert_edge(graph,9,5);
    insert_edge(graph,9,15);
    insert_edge(graph,10,6);
    insert_edge(graph,10,7);
    insert_edge(graph,10,11);
    insert_edge(graph,11,12);
    insert_edge(graph,11,13);
    insert_edge(graph,13,14);
    insert_edge(graph,13,18);
    insert_edge(graph,15,16);
    insert_edge(graph,16,17);
    insert_edge(graph,17,13);
    insert_edge(graph,17,20);
    insert_edge(graph,19,15);
    insert_edge(graph,20,21);

    print_graph(graph,NUM_V);
    
    init_indeg(graph,NUM_V,indeg);
    get_seq(graph, NUM_V, indeg, seq);
    for (i=0; i<NUM_V; i++) {
        printf("%d,", seq[i]);
    }
}

void print_graph(position graph, int nv) {
    int i;
    position p;
    for(i=0; i<nv; i++) {
        p = (graph + i)->next;
    printf("From %3d: ", i);
    while(p != NULL) {
        printf("%d->%d; ", i, p->element);
        p = p->next;
    }
    printf("\n");
    }
}
/*
 * insert an edge
 */
void insert_edge(position graph,int from, int to) 
{
    position np;
    position nodeAddr;
    
    np = graph + from;

    nodeAddr = (position) malloc(sizeof(struct node));
    nodeAddr->element = to;
    nodeAddr->next    = np->next;
    np->next = nodeAddr;    
}

void init_indeg(position graph, int nv, int indeg[]) {
    int i;
    position p;

    // initialize
    for(i=0; i<nv; i++) {
        indeg[i] = 0;
    }

    // update
    for(i=0; i<nv; i++) {
        p = (graph + i)->next;
        while(p != NULL) {
            (indeg[p->element])++;
            p = p->next;
        }
    }  
}

// update indeg when ver is removed
void update_indeg(position graph, int nv, int indeg[], int ver) {
    position p;
    p = (graph + ver)->next;
    while(p != NULL) {
        (indeg[p->element])--;
        p = p->next;
    }
}

/* find the vertice with 0 indegree*/
int find_next(int indeg[], int nv) {
    int next;
    int i;
    for(i=0; i<nv; i++) {
        if(indeg[i] == 0) break;
    }
    indeg[i] = -1;

    return i;
}

// return the sequence
void get_seq(position graph, int nv, int indeg[], int seq[]){
    int i;
    int ver;
    for(i = 0; i<nv; i++) {
        ver = find_next(indeg, nv);
        seq[i] = ver;
        update_indeg(graph, nv, indeg, ver);
    }
}
View Code

上面算法中有一个遍历所有节点的for循环,而循环中的find_next()函数也会遍历所有的节点。因此,算法复杂度为[$O(|V|^2)$]。

在find_next()中,我们将放入序列的节点入度记为-1。find_next()每次会遍历所有的节点,没有必要遍历入度为-1的节点。为了改善算法复杂度,我们可以使用一个队列(或者栈)来记录入度为0的元素。我们每次从队列中取出一个元素放入拓扑序列,并更新相邻元素的入度。如果该元素的相邻元素的入度变为0,那么将它们放入队列中。可以证明,这样的改造后,算法复杂度为[$O(|V|+|E|)$]

 

问题拓展

通过上面的算法,我们可以获得一个合法的科技发展序列。我随后想到一个问题,如何输出所有的科技发展序列呢?

这个问题的关键在于,某个时刻,可能同时有多个节点的入度同时为0。它们中的任何一个都可以成为下一个元素。为此,我们需要记录所有的可能性。

(我想到的,是复制入度数组和序列数组,以储存不同的可能性。不知道大家有什么更好的想法呢?)

 

总结

图,拓扑排序

 

欢迎继续阅读“纸上谈兵: 算法与数据结构”系列。

 

目录
相关文章
|
13天前
|
存储 弹性计算 人工智能
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
2025年9月24日,阿里云弹性计算团队多位产品、技术专家及服务器团队技术专家共同在【2025云栖大会】现场带来了《通用计算产品发布与行业实践》的专场论坛,本论坛聚焦弹性计算多款通用算力产品发布。同时,ECS云服务器安全能力、资源售卖模式、计算AI助手等用户体验关键环节也宣布升级,让用云更简单、更智能。海尔三翼鸟云服务负责人刘建锋先生作为特邀嘉宾,莅临现场分享了关于阿里云ECS g9i推动AIoT平台的场景落地实践。
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
|
4天前
|
云安全 人工智能 安全
Dify平台集成阿里云AI安全护栏,构建AI Runtime安全防线
阿里云 AI 安全护栏加入Dify平台,打造可信赖的 AI
|
12天前
|
人工智能 自然语言处理 自动驾驶
关于举办首届全国大学生“启真问智”人工智能模型&智能体大赛决赛的通知
关于举办首届全国大学生“启真问智”人工智能模型&智能体大赛决赛的通知
|
8天前
|
人工智能 运维 Java
Spring AI Alibaba Admin 开源!以数据为中心的 Agent 开发平台
Spring AI Alibaba Admin 正式发布!一站式实现 Prompt 管理、动态热更新、评测集构建、自动化评估与全链路可观测,助力企业高效构建可信赖的 AI Agent 应用。开源共建,现已上线!
759 23
|
7天前
|
人工智能 Java Nacos
基于 Spring AI Alibaba + Nacos 的分布式 Multi-Agent 构建指南
本文将针对 Spring AI Alibaba + Nacos 的分布式多智能体构建方案展开介绍,同时结合 Demo 说明快速开发方法与实际效果。
492 37
|
7天前
|
机器学习/深度学习 人工智能 搜索推荐
万字长文深度解析最新Deep Research技术:前沿架构、核心技术与未来展望
近期发生了什么自 2025 年 2 月 OpenAI 正式发布Deep Research以来,深度研究/深度搜索(Deep Research / Deep Search)正在成为信息检索与知识工作的全新范式:系统以多步推理驱动大规模联网检索、跨源证据。
474 41
|
1天前
|
文字识别 监控 物联网
这是我写的实施一地两检的跨境高铁站旅客资料预报系统的系统架构
本文设计了一套基于IAPIS理念的高铁跨境旅客预报与边检联动系统,覆盖青青草原内地与喜羊羊特别行政区间“一地两检”场景。系统在旅客购票后即采集证件、生物特征及行程信息,通过Advance Passenger Info Checker等模块,向出发地和目的地移民管理机构实时推送数据,实现出入境许可预审。支持线上/线下购票、检票、退票全流程管控,结合面部识别、行为追踪技术监控旅客状态,防止滞留或非法通行。列车发车前进行最终核验,确保所有跨境旅客获边检许可。若旅行被中途取消,系统自动改签、退票并通知各方,保障安全与效率。(239字)