欧拉路径问题

一.问题概述

欧拉路径是什么?

通过图中所有边的简单路。(换句话说,每条边都通过且仅通过一次)也叫”一笔画”问题。

通过图中所有边恰好一次且行遍所有顶点的通路称为欧拉通路。

 判定(充要条件)

  • 图是连通图,即不存在断连的点;
  • 若是无向图,则这个图的度数为奇数的点必须是0或2;
  • 若是有向图,要么所有的点的入度和出度相等,要么有且只有两个点的入度分别比出度大1或者小1。

二.算法思想

Hierholzer算法流程

1.对于无向图,判断度数为奇数点的个数,若为0,则设任意一点为起点,若为2,则从这两个点中任意取一个作为起点;

对于有向图,判断入度和出度不同的点的个数,若为2,则设入度比出度小1的点为起点,另一个点为终点。

2.从起点开始进行递归,对于当前节点x,扫描与x相连的所有边,当扫描到某一条边(x,y)时,删除该条边,并递归y,扫描完所有边后,将x加入队列。

3.倒序输出答案队列。

4.从起点开始,每一次执行递归函数,相当于模拟一笔画的过程。递归的边界显然就是路径的终点,对于一个有欧拉路径的图,此时图上的所有边都已被删除,自然就不能继续递归。由于存储答案是在遍历以后进行的,答案存储也就是倒序的,因此要倒序输出答案。

欧拉路径举例

三.leetcode——重新安排行程

1.题目出示

2.思路讲解

Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:

从起点出发,进行深度优先搜索。

每次沿着某条边从某个顶点移动到另外一个顶点的时候,都需要删除这条边。

如果没有可移动的路径,则将所在节点加入到栈中,并返回。

当我们顺序地考虑该问题时,我们也许很难解决该问题,因为我们无法判断当前节点的哪一个分支是「死胡同」分支。

不妨倒过来思考。我们注意到只有那个入度与出度差为 1 的节点会导致死胡同。而该节点必然是最后一个遍历到的节点。我们可以改变入栈的规则,当我们遍历完一个节点所连的所有节点后,我们才将该节点入栈(即逆序入栈)。

对于当前节点而言,从它的每一个非「死胡同」分支出发进行深度优先搜索,都将会搜回到当前节点。而从它的「死胡同」分支出发进行深度优先搜索将不会搜回到当前节点。也就是说当前节点的死胡同分支将会优先于其他非「死胡同」分支入栈。

这样就能保证我们可以「一笔画」地走完所有边,最终的栈中逆序地保存了「一笔画」的结果。我们只要将栈中的内容反转,即可得到答案。

3.算法思想

本题思路

  • 必须从JFK机场出发;
  • 字典序,每次有限走字典小的路径;
  • 每张票最少保证用且用一次,所以每走完一次需要擦除走过的路。

Hierholzer算法流程

  • 选择任一顶点为起点,遍历所有相邻边;
  • 深度搜索,访问相邻顶点,将经过的边都删除;
  • 如果当前顶点没有相邻边,则将顶点入栈;
  • 栈中的顶点倒序输出,就是从起点出发的欧拉路径。

4.代码实现

// 将字符串形式的机场代码转换为整数ID  
int str2id(char* a) {
    int ret = 0;
    for (int i = 0; i < 3; i++) {
        // 假设机场代码为3个字母,通过累加26的次方和字符与'A'的差值来计算ID  
        ret = ret * 26 + a[i] - 'A';
    }
    return ret;
}

// 比较函数,用于qsort排序,按照起始机场ID逆序、目的地机场ID逆序排序  
int cmp(const void* _a, const void* _b) {
    int** a = (int**)_a, ** b = (int**)_b;
    // 如果起始机场ID不同,则直接返回差值;如果相同,则比较目的地机场ID  
    return (*b)[0] - (*a)[0] ? (*b)[0] - (*a)[0] : (*b)[1] - (*a)[1];
}

// 主函数,构建旅行路线  
char** findItinerary(char*** tickets, int ticketsSize, int* ticketsColSize, int* returnSize) {
    // 初始化每个机场的目的地列表长度为0  
    memset(vec_len, 0, sizeof(vec_len));

    // 为栈分配内存,用于存储访问顺序  
    stk = malloc(sizeof(int) * (ticketsSize + 1));
    stk_len = 0;

    // 临时数组,用于存储机票信息的ID形式  
    int* tickets_tmp[ticketsSize];
    for (int i = 0; i < ticketsSize; i++) {
        // 为每张机票的ID形式分配内存  
        tickets_tmp[i] = (int*)malloc(sizeof(int) * 2);
        // 将机票信息的字符串形式转换为ID形式  
        tickets_tmp[i][0] = str2id(tickets[i][0]);
        tickets_tmp[i][1] = str2id(tickets[i][1]);
        // 保存ID对应的字符串到id2str数组中,便于后续恢复字符串形式  
        id2str[tickets_tmp[i][0]] = tickets[i][0];
        id2str[tickets_tmp[i][1]] = tickets[i][1];
    }

    // 使用qsort对机票ID信息进行排序  
    qsort(tickets_tmp, ticketsSize, sizeof(int*), cmp);

    // 遍历排序后的机票ID信息,构建每个机场的目的地列表  
    int add = 0;
    while (add < ticketsSize) {
        int adds = add + 1, start = tickets_tmp[add][0];
        // 找到所有以start为起始机场的机票  
        while (adds < ticketsSize && start == tickets_tmp[adds][0]) {
            adds++;
        }
        // 设置起始机场的目的地列表长度  
        vec_len[start] = adds - add;
        // 为起始机场的目的地列表分配内存  
        vec[start] = malloc(sizeof(int) * vec_len[start]);
        // 将目的地ID填充到列表中  
        for (int i = add; i < adds; i++) {
            vec[start][i - add] = tickets_tmp[i][1];
        }
        // 更新add,处理下一个起始机场  
        add = adds;
    }

    // 从"JFK"机场开始进行深度优先搜索,构建访问顺序  
    dfs(str2id("JFK"));

    // 设置返回数组的长度  
    *returnSize = ticketsSize + 1;
    // 为返回的字符串数组分配内存  
    char** ret = malloc(sizeof(char*) * (*returnSize));
    // 将栈中的机场ID转换回字符串形式,并逆序存入返回的数组中  
    for (int i = 0; i < *returnSize; i++) {
        ret[(*returnSize) - 1 - i] = id2str[stk[i]];
    }

    // 释放临时数组占用的内存  
    for (int i = 0; i < ticketsSize; i++) {
        free(tickets_tmp[i]);
    }

    // 返回构建的旅行路线数组  
    return ret;
}

四.性能分析

复杂度分析

时间复杂度:O(mlog⁡m),其中 m 是边的数量。对于每一条边我们需要 O(logm) 地删除它,最终的答案序列长度为 m+1,而与 n 无关。

空间复杂度:O(m),其中 m 是边的数量。我们需要存储每一条边。

  • 58
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

傲_慢_之_最

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值