栈
栈
题目背景
栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表。
栈有两种最重要的操作,即 pop(从栈顶弹出一个元素)和 push(将一个元素进栈)。
题目描述

考虑的是这样一个问题:一个操作数序列,$1,2,\ldots ,n$(图示为 1 到 3 的情况),栈 A 的深度大于 $n$。
现在可以进行两种操作,
- 将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 push 操作)
- 将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 pop 操作)
使用这两种操作,由一个操作数序列就可以得到一系列的输出序列,下图所示为由 1 2 3 生成序列 2 3 1 的过程。

(原始状态如上图所示)
你的程序将对给定的 $n$,计算并输出由操作数序列 $1,2,\ldots,n$ 经过操作可能得到的输出序列的总数。
输入格式
输入文件只含一个整数 $n$($1 \leq n \leq 18$)。
输出格式
输出文件只有一行,即可能输出序列的总数目。
样例 #1
样例输入 #1
3
样例输出 #1
5
Answer
这个问题可以通过动态规划或递归的方式来解决,关键在于理解栈的操作规则如何决定了可能的输出序列。需要考虑的是,在给定操作数序列 $1,2,\ldots,n$ 的情况下,可以通过不同的 push 和 pop 操作顺序得到多少种不同的输出序列。
解题思路
由于栈是后进先出(LIFO)的结构,因此在任何时刻,如果栈不为空,最后一个进入栈的元素必须在之前所有进入栈的元素之前或之后出栈,这就创建了一个子问题的结构。
可以使用递归或动态规划的方法来解决这个问题。为了简化问题,可以考虑使用递归的方法,其中每一步都基于是否 push 或 pop 来决定下一步的操作。但是,直接这么做的话,会有很多重复计算,所以更有效的方法是使用动态规划来减少重复计算。
动态规划解法
动态规划的状态可以定义为 $dp[i][j]$,其中 i 表示尚未处理(即尚未进栈)的数字数量,j 表示当前栈中的元素数量。状态转移方程反映了两种操作:
- push 操作:将下一个数 push 到栈中,此时栈中元素的数量增加,而尚未入栈的元素减少。
- pop 操作:从栈中 pop 出一个数到输出序列,此时栈中元素的数量减少,尚未入栈的元素不变。
每个状态的值表示当前状态下,可以生成的不同输出序列的总数。
实现
由于这个问题具有较好的子结构特性,可以使用记忆化搜索(一种递归加缓存的技术)来简化实现,同时避免重复计算。
在这个实现中,初始化了一个动态规划数组 dp 来存储中间结果,初始值设为 -1 表示该状态尚未计算。findSequences 函数递归地计算在给定还剩下多少个数没有进栈和当前栈中有多少个数的情况下,可以生成的不同输出序列的总数。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;
public class Main {
private static long[][] dp;
// 用 dp[i][j] 来表示状态,其中 i 表示尚未处理(即尚未进栈)的数字数量,j 表示当前栈中的元素数量
private static long findSequences(int n, int inStack) {
if (n == 0 && inStack == 0) return 1;
if (n < 0 || inStack < 0) return 0;
if (dp[n][inStack] != -1) return dp[n][inStack];
long count = 0;
if (inStack > 0) count += findSequences(n, inStack - 1);
count += findSequences(n - 1, inStack + 1);
dp[n][inStack] = count;
return count;
}
public static void main(String[] args) throws IOException {
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(bf.readLine());
int n = Integer.parseInt(st.nextToken());
dp = new long[n + 1][n + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
dp[i][j] = -1;
}
}
System.out.println(findSequences(n, 0));
}
}
Answer2
卡特兰数是组合数学中一个重要的数列,在许多计数问题中都有应用,比如本题中求由操作数序列 $1, 2, \ldots, n$ 通过栈操作可能得到的输出序列总数。
建立数组f。f[i]表示i个数的全部可能性。
f[0] = 1, f[1] = 1; //当然只有一个
设 x 为当前出栈序列的最后一个,则x有n种取值
由于x是最后一个出栈的,所以可以将已经出栈的数分成两部分
比x小
比x大
比x小的数有x-1个,所以这些数的全部出栈可能为f[x-1]
比x大的数有n-x个,所以这些数的全部出栈可能为f[n-x]
这两部分互相影响,所以一个x的取值能够得到的所有可能性为f[x-1] * f[n-x]
另外,由于x有n个取值,所以
ans = f[0]*f[n-1] + f[1]*f[n-2] + … + f[n-1]*f[0];
卡特兰数的定义
卡特兰数 $C_n$ 可以通过下面的公式定义:
$ C_n = \frac{1}{n+1}\binom{2n}{n} = \binom{2n}{n} – \binom{2n}{n-1} \quad \text{for } n \geq 0 $
也可以递归地定义为:
$ C0 = 1, \quad C{n+1} = \sum_{i=0}^{n}CiC{n-i} \quad \text{for } n \geq 0 $
实现
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
class Main {
static StreamTokenizer token = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public static void main(String[] args) {//catalan
int n = nextInt();
long[] arr = new long[n + 1];
arr[0] = 1;
for (int i = 0; i < n; i++) {
for (int k = 0; k <= i; k++) {
arr[i + 1] += arr[k] * arr[i - k];
}
}
PrintWriter pr = new PrintWriter(new OutputStreamWriter(System.out));
pr.println(arr[n]);
pr.flush();
}
public static int nextInt() {
try {
token.nextToken();
} catch (IOException e) {
e.printStackTrace();
}
return (int) token.nval;
}
}
使用了第二种递归定义方法来计算卡特兰数。
arr 数组用来存储从 C_0 到 C_n 的所有卡特兰数的值。初始时,arr[0] 被设置为 1,符合卡特兰数的定义。
接下来的循环通过卡特兰数的递归定义计算出所有需要的卡特兰数值。arr[i+1] 被计算为之前所有可能的 arr[k] * arr[i-k] 的和,符合卡特兰数的递归公式。
nextInt 函数使用 StreamTokenizer 从标准输入读取整数,用于读取题目中给定的 n 值。
最终,通过 PrintWriter 将计算出的 arr[n] 输出,即为给定操作数序列 1, 2, ..., n 可能的输出序列的总数。