括号匹配

源自2017.3.17参加网易游戏实习面试时的面试题,题目为:给定N对括号”()”,有多少种合法的匹配方式?

简单递归

当时的思路:递归,设定两个初始变量leftL,leftR,分别表示,当前还剩下的左右括号数目,如果左右括号数均为0,那么表示匹配完毕;否则,当前选择左括号,leftL - 1,进入下一层递归,当前选择右括号,leftR - 1,进入下一层递归,两者的匹配数目相加即可!
边界情况:leftL或者leftR < 0,以及leftL > leftR时,此时匹配已经不合法,返回,相当于剪枝。
代码如下:

int totalCount = 0;

void matchNum(int leftL, int leftR)
{
    //剪枝(剩余的左括号数必须小于右括号数)
    if (leftL > leftR || leftL < 0 || leftR < 0)
    {
        return;
    }
    if (leftL == 0 && leftR == 0)
    {
        totalCount++;
        return;
    }
    matchNum(leftL - 1, leftR);
    matchNum(leftL, leftR - 1);
}

由于存在重复计算,算法复杂度为O(2^N),当N超过15时,在PC上(I7-4790)运行时间超过10S;

带备忘录的递归

上述的递归中,涉及大量的重复计算,如下图(以N=4为例):
这里写图片描述
可以通过建立一个备忘录,记录已经计算的结点,递归到某一层时,先查询备忘录是否已经计算,如果是,直接返回备忘录中的值,否则进行递归计算。这里,使用std::map存储结点,代码如下:

std::map<std::pair<int, int>, long long> umap;

long long matchNum1(int leftL, int leftR)
{
    //剪枝(剩余的左括号数必须小于右括号数)
    if (leftL > leftR || leftL < 0 || leftR < 0)
    {
        return 0;
    }
    if (leftL == 0 && leftR == 0)
    {
        //totalCount++;
        return 1;
    }
    long long result = 0;
    if (umap.find(std::pair<int, int>(leftL - 1, leftR)) != umap.end())
    {
        result += umap[std::pair<int, int>(leftL - 1, leftR)];
    }
    else
    {
        long long  tmp = matchNum1(leftL - 1, leftR);
        umap[std::pair<int, int>(leftL - 1, leftR)] = tmp;
        result += tmp;
    }

    if (umap.find(std::pair<int, int>(leftL, leftR - 1)) != umap.end())
    {
        result += umap[std::pair<int, int>(leftL, leftR - 1)];
    }
    else
    {
        long long tmp = matchNum1(leftL, leftR - 1);
        umap[std::pair<int, int>(leftL, leftR - 1)] = tmp;
        result += tmp;
    }

    return result;
}

这里每次查询时间为O(logN),计算的结点数为O(N^2),总的时间复杂度为O(LogN*N^2);如果想要在O(1)时间内完成查询,需要使用std::unordered_map,但如果使用std::unordered_map,必须要对此处的std::pair< int, int >创建自定义的hash函数和等价准则,对此可以参考博客
代码如下:

#include <functional>

template<typename T>
inline void hash_combine(std::size_t& seed, const T& val)
{
    seed ^= std::hash<T>()(val)+0x9e3779b9 + (seed << 6) + (seed >> 2);
}

template<typename T>
inline void hash_val(std::size_t& seed, const T& val)
{
    hash_combine(seed, val);
}

template<typename T, typename... Types>
inline void hash_val(std::size_t& seed, const T& val, const Types&... args)
{
    hash_combine(seed, val);
    hash_val(seed, args...);
}

template<typename... Types>
inline std::size_t hash_val(const Types& ...args)
{
    std::size_t seed = 0;
    hash_val(seed, args...);
    return seed;
}


std::size_t hash(const std::pair<int, int>& c)
{
    return  hash_val(c.first, c.second);
};

bool eq(const std::pair<int, int>& c1, const std::pair<int, int>& c2)
{
    return c1.first == c2.first && c1.second == c2.second;
};

 std::unordered_map<std::pair<int, int>, long long, decltype(&hash), decltype(&eq)> 
    umap(100, hash, eq);

long long matchNum1(int leftL, int leftR)
{
    //剪枝(剩余的左括号数必须小于右括号数)
    if (leftL > leftR || leftL < 0 || leftR < 0)
    {
        return 0;
    }
    if (leftL == 0 && leftR == 0)
    {
        //totalCount++;
        return 1;
    }
    long long result = 0;
    if (umap.find(std::pair<int, int>(leftL - 1, leftR)) != umap.end())
    {
        result += umap[std::pair<int, int>(leftL - 1, leftR)];
    }
    else
    {
        long long  tmp = matchNum1(leftL - 1, leftR);
        umap[std::pair<int, int>(leftL - 1, leftR)] = tmp;
        result += tmp;
    }

    if (umap.find(std::pair<int, int>(leftL, leftR - 1)) != umap.end())
    {
        result += umap[std::pair<int, int>(leftL, leftR - 1)];
    }
    else
    {
        long long tmp = matchNum1(leftL, leftR - 1);
        umap[std::pair<int, int>(leftL, leftR - 1)] = tmp;
        result += tmp;
    }

    return result;
}

动态规划方式

令剩余的左括号数为L,剩余的右括号数为R,那么可得递推式:F(L,R) = F(L - 1,R) + F(L, R - 1)。
完整的递归式为:
(1)F(L,R) = 0,L < 0 or R < 0 or L > R
(2)F(L,R) = 1,L = 0 and R = 0
(3)F(L,R) = F(L - 1,R) + F(L, R - 1), others

当N = 4时,动规的表格如下(行为L,列为R):
这里写图片描述
可以使用(N+1)^2的空间用于记录计算过的值,但是这里使用(N+1)就够了,代码如下:

long long matchNum2(int num)
{
    if (num == 0)
    {
        return 1;
    }
    std::vector<long long> dp(num + 1, 1);
    for (int i = 1; i <= num; ++i)
    {
        for (int k = i + 1; k <= num; ++k)
        {
            dp[k] += dp[k - 1];
        }
    }
    return dp[num];
}

时间对比(Release模式)

N = 18时;
这里写图片描述
当N > 20时,普通递推已经无法短时间计算出来,故不参加以下对比:
N = 5998时,
这里写图片描述
(计算结果为负数,是因为溢出了!)
可以发现,动态规划计算速度比递归快了很多;
此外,当N大于某个值时,递归会因栈空间不足而崩溃,因此,使用动态规划的方式不仅在时间,而且在代码的强健性上也强于递归!

卡特兰数

在网上搜索关于括号匹配的信息时,看到这个问题可以用卡特兰数解决,关于卡特兰数,可以转至链接.

注:以下内容参考博客

上面的方法,是基于剩余的左右括号数量来得到递归式的,那么有没有另外的思考方式呢?

现在,如果我们仅仅考虑左括号,而不考虑右括号,通过下面的示意图可以看出,我们可以将整个问题,拆分成一个个子问题,然后将子问题合并就可以解决整个问题。
这里写图片描述
这里a[i]的含义是:第i个左括号左(右)边的括号的匹配数,a[i] * a[N - 1 - i]表示当前左括号在位置i+1时,可以得到的匹配数(不知道这样表述清不清楚,看图应该能更加明白)。
递推式为:
F(N) = F(i) * F(N-1-i),0 <= i <= N - 1;
当i = 0或者1时,F(i) = 1;
对应的代码如下:

long long matchNum3(int num)
{
    if (num == 0 || num == 1)
    {
        return 1;
    }
    std::vector<long long> dp(num + 1, 1);
    for (int i = 2; i <= num; ++i)
    {
        long long tmp = 0;
        for (int k = 0; k < i; ++k)
        {
            tmp += dp[k] * dp[i - 1 - k];
        }
        dp[i] = tmp;
    }
    return dp[num];
}

两种动态规划方法的时间对比:

这里写图片描述
N = 115998时,
计算结果由于溢出,可以忽略;前者是自己写的动态规划方法计算时间,后者是卡特兰数的动态规划方法的计算时间,可以发现前者约是后者时间的 1/2,在实验了其它的N值后,基本都是在1/2左右,为什么会这样暂时不去分析了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值