您的位置:首页 > 编程语言 > Go语言

Catalan数和应用 & 2015 google APAC round 2 problem D 括号配对问题

2014-09-30 20:31 253 查看
Catalan数是排列组合中很重要的一种数序列,我们先举一个排列组合数的应用来说明Catalan数的意义和如何构造Catalan数,以下例子从《离散数学与组合数学》中选出:

对于一个N*N的网格,从左下角移动到右上角,每一步只能向右或者向上,我们知道这有C(2N,N)种走法,因为一共要走2N步,从中选出N步向上(或者向右),得到C(2N,N)。那么如果我们加上一个条件,就是走的路径永远不能向上超出左下->右上的对角线,问能有多少种走法?



这个问题可以转换成,N个“右”和N个“上”组成的2N个移动过程,其中在任一步,左边的“右”的数量都要大于等于“上”,否则就会超出对角线。我们知道,符合条件的解的数量等于总解数减去不符合条件的序列数量,那么不符合条件的序列,即在某一点左边的“上”数量比“右”大的序列有几种呢?

举一个5*5网格的例子,一种不符合条件的走法是“右上上右上上上右右右”,可以看到第一个超过对角线的时刻出现在第三步,如果我们将第三部之后的所有步数的“右”与“上”互换,那么就变成“右上上上右右右上上上”,也就是对应了一种4个“右”,6个“上”的序列,每一种不符合条件的走法根据这种方法,都能唯一地转换为4个“右”,6个“上”的序列。反过来,对于任何一个4个“右”,6个“上”的序列,我们先找到“上”比“右”恰好多一个的位置,然后把序列右边的“上”和“右”互换,也能唯一地转换为5个“右“,5个”上“,且一定会超出对角线的走法。也就是说,5个“右“,5个”上“,超过对角线的走法与任意4个“右”,6个“上”的走法可以一一对应,那么数量也就相等,也就是说,满足条件的走法共有C(10,5)
- C(10,4)种。而对于N*N网格,满足条件的走法有C(2N,N) - C(2N,N-1)种,这也就给出了Catalan数的定义,第N个Catalan数等于C(2N,N) - C(2N,N-1)。

有很多问题与以上问题完全等价,其答案就是Catalan数:

1. N个0与N个1构成的序列,满足条件任何位置左边的0个数都要大于等于1的个数

2. 单种括号配对问题,()()(),任何位置左边的左括号数量都要大于等于右括号的数量

3. 队列中的数进栈,问有多少种合法的出栈顺序,比如说ABCD的出栈顺序永远不可能为CABD。不妨将整个操作看成N个进栈和N个出栈操作的序列,不同的进栈出栈顺序对应了不同的输出队列,而任何位置左侧的进栈操作数量都要大于等于出栈数量

4.将2N个不重复数字排成N*2两行,满足任何一行左边的元素小于右边,任何一列上面的元素小于下面,这同样可以转换成对于已排序的2N个数字,分配N个”上“和N个”下“,保证任何一个位置左侧的”上“数量大于等于”下“。

今年google APAC第二轮第四题就利用了Catalan数的原理,对于N个左括号和N个右括号构成的合法序列,求按照字典序的第K个序列,题目如下:



对于这道题,不但需要求解第N个Catalan数的大小,还要深入其内部求字典序,为了递推求解每个位置是左括号还是右括号,在这里我引入了一个Catalan数的变体Catalan(L,R),代表序列的左侧已经满足要求的情况下,剩下L个左括号和R个右括号(L<=R),问有几种合法排列顺序。由于左边的序列已经确保满足问题,我们不妨从右边看这串L个左括号和R个右括号的序列,任何一个位置右侧的右括号必须大于等于左括号数,模仿上面的证明方式,这串括号有C(L+R,L) - C(L+R,L-1)种排列方式。在这道题中我简历了一个矩阵用来存储L,R的排列数量,对于N+N的括号序列的第K个字典序,只要递推求解每个位置究竟是左括号还是右括号就能得到最终的序列。这道题中因为数值很大,处在long
long型溢出边缘,因此我的递推乘法用了商+余数的方式来保证既不丢失小数也不溢出,程序如下:

#include <fstream>
using namespace std;
#define MAXK 1000000000000000001LL//题目要求的取模除数
//存放Catalan数的矩阵
unsigned long long count[101][101];// [left parentheses count][right parentheses count]

ifstream fin("D-large-practice.in");
ofstream fout("output.txt");

//生成Catalan数矩阵
void generate() {
for (int j = 0; j < 101; j++) {
count[0][j] = 1LL;
count[1][j] = j;
count[2][j] = (j+2) * (j+1) / 2 - j - 2;
}
for (int i = 3; i < 101; i++) {
for (int j = i; j < 101; j++) {
<span style="white-space:pre">			</span>//利用count[i-1][j]求出count[i][j],为了不溢出且不丢失精度,用了余数和商的做法
unsigned long long mod = count[i-1][j] % ((j+2-i) * i);
unsigned long long quotient = count[i-1][j] / (j+2-i) / i;
count[i][j] = quotient * (j+1-i) * (j+i) + mod * (j+1-i) * (j+i) / ((j+2-i) * i);
if ((count[i][j] > MAXK)) {
count[i][j] =  MAXK;
}
}
}

}

//根据Catalan数矩阵,递推求出每个位置是左括号还是右括号
void walk(int left, unsigned long long index) {
int right = left;
if (count[left][right] < index) {
fout << "Doesn't Exist!" << endl;
return;
} else {
while (index > 0 && left > 0) {
if (count[left-1][right] >= index) {
fout << "(";
left--;
} else {
fout << ")";
index -= count[left-1][right];
right--;
}
}
for (int i = right; i > 0; i --) {
fout << ")";
}
fout << endl;
}
}

int main() {
generate();
int T, left;
unsigned long long index;
fin >> T;
for (int t = 1; t <= T; t++) {
fin >> left >> index;
fout << "Case #" << t << ": ";
walk(left, index);
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: