单词接龙

  最近碰到了一个比较有意思的算法题 —— 单次接龙,我尝试做了一下,虽然能解,但是时间复杂度让我很不满意,于是google了网友的解法,找到一个比较满意的算法 —— 链接,作者是用JAVA实现的,我在此将它转换为C++实现。

  

问题描述

  拉姆刚开始学习英文单词,对单词排序很感兴趣。如果给拉姆一组单词,他能够迅速确定是否可以将这些单词排列在一个列表中,使得该列表中任何单词的首字母与前一单词的尾字母相同。你能编写一个程序来帮助拉姆进行判断吗?

函数接口

bool CanArrayWords(int n, string arr[])
{
    ...
}

  

暴力求解

  我最先想到的方法是:首先,将输入字符串数组进行全排列,然后验证每一种排列直到找到一种排列符合“单次接龙”,或者遍历各种排列也找不到符合“单词接龙”的排列。
  这种解法,是可以判断是否符合“单词接龙”的,但是,它的时间复杂度是 O(n!),极其糟糕!

  

欧拉路径

  网友提供了一个更高效的算法(链接),它的核心思想就是:将每一个单次看成是有向图的一条边,首尾字母看作顶点,那么这个问题就转化为了判断一个有向图是否可以表示为一条欧拉路径(不闭合)或欧拉回路的一笔画问题

欧拉路径的判断定理

  一个连通的有向图可以表示为一条从起点到终点的(不闭合的)欧拉路径的充要条件是: 起点的出度比入度多1, 终点的入度比出度多1,而其它顶点的入度和出度都相等。
  一个连通的有向图可以表示为一条欧拉回路的充要条件是:每个顶点的入度和出度都相等

  

C++代码实现如下:

bool CanArrayWords(int n, string arr[])
{
    // 26个英文字母看作26个点,用整数0-25来表示
    int directedGraph[26][26] = {};    // 邻接矩阵表示有向图
    int inDegree[26] = {};             // 顶点入度
    int outDegree[26] = {};            // 顶点出度
    bool hasLetter[26] = {};           // 标记字母是否出现过
    bool hasEuler = false;             // 有狭义欧拉路径或欧拉回路标志

    // 遍历数组,填涂邻接矩阵
    for (int i = 0; i < n; i++)
    {
        string word = arr[i];
        char firstLetter = word.at(0);
        char lastLetter = word.at(word.length() - 1);
        outDegree[firstLetter - 'a']++;
        inDegree[lastLetter - 'a']++;
        directedGraph[firstLetter - 'a'][lastLetter - 'a'] = 1; // 有向图
        hasLetter[firstLetter - 'a'] = true;
        hasLetter[lastLetter - 'a'] = true;
    }

    // 找出起点和终点
    int startNum = 0;
    int endNum = 0;
    for (int vertex = 0; vertex < 26; vertex++)
    {
        if (outDegree[vertex] - inDegree[vertex] == 1)    // 起点
            startNum++;
        if (inDegree[vertex] - outDegree[vertex] == 1)    // 终点
            endNum++;
        if (abs(inDegree[vertex] - outDegree[vertex]) > 1)
        {
            hasEuler = false;
            return false;
        }
    }
    bool isEulerPath = (startNum == 1 && endNum == 1);   // 这里指狭义上的欧拉路径,不包括欧拉回路
    bool isEulerCircuit = (startNum == 0 && endNum == 0);// 欧拉回路
    if ((!isEulerPath) && (!isEulerCircuit))    // 既不是欧拉路径也不是欧拉回路
        hasEuler = false;
    else
        hasEuler = true;
#if 0
    // 判断是否弱连通
    int vertexNum = 0;    // 统计图中点的个数
    for (int letter = 0; letter < 26; letter++)
    {
        if (hasLetter[letter])
            vertexNum++;
    }
    int firstWordFirstLetter = arr[0].at(0) - 'a';// 以第一个单词的首字母作为起点进行BFS
    hasEuler = hasEuler && isConnected(firstWordFirstLetter, vertexNum, directedGraph);
#endif

    return hasEuler;
}

  

测试

  我们可以用以下四组数据来进行测试:

testing data 1:   3 abc cdefg ghijkl              result: true
testing data 2:   4 abc cdef fghijk xyz           result: false
testing data 3:   4 abc cde cfg ghc               result: true
testing data 4:   3 aba cdc efe                   result: false

  

连通性检查

  上述四组测试数据,前三组能通过测试,第四组不能通过。很明显,第四组数据表示的有向图,是不连通的图。而欧拉路径的判断定理中,特别强调连通性。为此,需要将上述代码注释的检查弱连通性的部分放开,并实现以下函数。

// 判断有向图是否弱连通,即转换成无向图判断是否连通
bool isConnected(int start, int vertexNum, int directedGraph[][26])
{
    int undirectedGraph[26][26] = {};
    for (int i = 0; i < 26; i++)     // 把有向图转换成无向图
    {
        for (int j = 0; j < 26; j++)
        {
            if (directedGraph[i][j] == 1)
            {
                undirectedGraph[i][j] = 1;
                undirectedGraph[j][i] = 1;
            }
        }
    }
    queue<int> iQueue;
    bool passedVertex[26] = {};
    int passedVertexNum = 0;
    iQueue.push(start);
    // 从起点开始进行BFS,统计遍历到点的个数
    while (!iQueue.empty())
    {
        int currentVertex = iQueue.front();
        iQueue.pop();
        passedVertex[currentVertex] = true;
        passedVertexNum++;
        for (int vertex = 0; vertex < 26; vertex++)
        {
            if (undirectedGraph[currentVertex][vertex] == 1 && passedVertex[vertex] == false)
                iQueue.push(vertex);
        }
    }
    // 遍历到所有的点,证明无向图是连通的
    if (passedVertexNum == vertexNum)
        return true;
    else
        return false;
}

  

简化版

  上述的代码稍显复杂,如果在面试中不一定能够非常流畅的写出来,为此,我们可以先不考虑“连通性”,写出一个“简化版”的单词接龙程序,如下:

#define ALPHABET_SIZE 26

bool CanArrayWords(int n, string strArray[])
{
    int inDegree[ALPHABET_SIZE] = {};
    int outDegree[ALPHABET_SIZE] = {};

    // 填充邻接矩阵,并计算各顶点出度和入度
    for (int i = 0; i < n; ++i) {
        outDegree[strArray[i].front() - 'a']++;
        inDegree[strArray[i].back() - 'a']++;
    }

    // 欧拉路径中,各顶点出度和入度要么相等,要么差1(起点和终点)
    int startVertex = -1, endVertex = -1;
    for (int i = 0; i < ALPHABET_SIZE; ++i) {
        if (abs(outDegree[i] - inDegree[i]) > 1)
            return false;

        // 起点
        if (outDegree[i] - inDegree[i] == 1) {
            if (startVertex == -1)
                startVertex = i;
            else
                return false;
        }

        // 终点
        if (outDegree[i] - inDegree[i] == -1) {
            if (endVertex == -1)
                endVertex = i;
            else
                return false;
        }

    }

    return true;
}

  

展开阅读全文

没有更多推荐了,返回首页