P1722 矩阵 II 题解: 1) DFS深度优先遍历 2)卡特兰数(Catalan number) 3)动态规划
P1722 矩阵 II : https://www.luogu.com.cn/problem/P1722
题目理解:
题目要求我们求解有多少种方式能够在一个 1 × 2n 的矩阵中放置红色和黑色算筹,使得矩阵满足“平衡”条件。具体来说,要求:
- 红色算筹和黑色算筹的数量必须相等。
- 在前
i格中,红色算筹的数量必须大于或等于黑色算筹的数量。
思路分析:
思路一: DFS 深度优先算法
我们可以将问题视为一种 合法的括号序列问题,其中:
- 红色算筹(
r)充当“左括号”((), - 黑色算筹(
b)充当“右括号”())。
为了保持平衡,我们需要保证在任何前缀中,红色算筹的数量不小于黑色算筹的数量,这正是合法括号序列的条件。最终的合法序列长度为 2n,其中红色算筹与黑色算筹数量相等。
关键点:
- 递归转移:每次我们可以选择放置红色算筹或者黑色算筹,在放置黑色算筹时,必须保证红色算筹数量大于或等于黑色算筹。
- 递归中的计数:使用 DFS 来枚举所有可能的排列,通过动态规划缓存结果,避免重复计算。
问题中的现有代码分析:
- 递归函数
dfs(m, cnt):递归枚举当前放置的算筹,m是当前的位置,cnt是当前放置的红色算筹的数量。 - 状态转移:
r++表示放置一个红色算筹,b++表示放置一个黑色算筹,满足条件的情况下递归调用。
修改与改进:
根据题目要求,需要根据“红色算筹和黑色算筹数量相等且红色算筹数量不小于黑色算筹数量”的条件来修改现有代码,下面是改进后的实现:
#include <bits/stdc++.h>
using namespace std;
int n, v[205][205], r, b;
const int MOD = 100;
int dfs(int m, int cnt) {
// m 代表当前的位置,cnt 代表已经放置的红色算筹的数量
if (m == 2 * n) { // 如果已经放置了 2n 个算筹,说明完成了一个合法的排列
return (cnt == n); // 只有当红色算筹数量等于黑色算筹数量时,才是有效的
}
if (v[m][cnt] != -1) { // 如果已经计算过,直接返回结果
return v[m][cnt];
}
v[m][cnt] = 0; // 初始化
// 放置红色算筹
if (r < n) {
r++;
v[m][cnt] = (v[m][cnt] + dfs(m + 1, cnt + 1)) % MOD;
r--;
}
// 放置黑色算筹,条件是红色算筹数量大于等于黑色算筹数量
if (b < n && cnt > b) {
b++;
v[m][cnt] = (v[m][cnt] + dfs(m + 1, cnt)) % MOD;
b--;
}
return v[m][cnt];
}
int main() {
cin >> n;
memset(v, -1, sizeof(v)); // 初始化缓存数组
cout << dfs(0, 0) << endl; // 从第 0 个位置开始,已放置的红色算筹数量为 0
return 0;
}
主要修改与改进:
- 状态缓存:
v[m][cnt]被初始化为-1表示尚未计算过,用于动态规划缓存。 - 放置红色算筹条件:当
r < n时可以放置红色算筹。 - 放置黑色算筹条件:当
b < n且当前黑色算筹数量b小于红色算筹数量cnt时可以放置黑色算筹。 - 终止条件:当
m == 2 * n时,如果红色和黑色算筹的数量相等,即cnt == n,说明这是一个有效的排列。
时间复杂度分析:
- 每个递归状态
v[m][cnt]只会计算一次,因此总体时间复杂度为 O(n^2)。
输入输出示例:
输入:
2
输出:
2
方案解释:
- 红、黑、红、黑
- 红、红、黑、黑
这些方案是合法的,满足红色算筹的数量大于等于黑色算筹的数量,最终方案数量对 100 取模后的结果为 2。
思路二: 卡特兰数(Catalan number)数列
这个问题可以使用卡特兰数来解决。卡特兰数(Catalan number)是一个数列,它在组合数学中有许多应用,其中一种应用就是计算在给定的条件下,红色和黑色算筹的排列方式。
对于这个问题,我们需要找到一个 1×2n 的矩阵中,红色和黑色算筹数量相等且任意前缀中红色算筹数量不小于黑色算筹数量的排列方式。这正好是卡特兰数的一个经典应用。
第 n 个卡特兰数 ( C_n ) 的公式为:
$$C_n = \sum_{i=0}^{n - 1} C_i C_{n - 1 - i}$$
我们需要计算 ( C_n ) 并对 100 取模。
C++ 代码实现
#include <iostream>
#include <vector>
using namespace std;
int catalan(int n) {
vector<int> C(n + 1, 0);
C[0] = 1;
C[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 0; j < i; j++) {
C[i] = (C[i] + C[j] * C[i - j - 1]) % 100;
}
}
return C[n];
}
int main() {
int n;
cin >> n;
cout << catalan(n) << endl;
return 0;
}
代码解释
- 定义卡特兰数数组:我们使用一个动态规划数组
C来存储卡特兰数,其中C[i]表示第 i 个卡特兰数。 - 初始化:
C[0]和C[1]都初始化为 1,因为 ( C_0 = 1 ) 和 ( C_1 = 1 )。 - 动态规划计算:使用两层循环来计算
C[i],其中外层循环从 2 到 n,内层循环从 0 到 i-1。根据卡特兰数的递推公式 ( C_i = \sum_{j=0}^{i-1} C_j C_{i-j-1} ),我们更新C[i]。 - 取模:在计算过程中,我们对 100 取模,以确保结果在 100 以内。
- 输出结果:最后输出
C[n],即第 n 个卡特兰数对 100 取模后的结果。
样例验证
对于输入 ( n = 2 ):
C[0] = 1C[1] = 1C[2] = C[0] * C[1] + C[1] * C[0] = 1 * 1 + 1 * 1 = 2
因此,输出为 2,与题目中的样例一致。
时间复杂度
该算法的时间复杂度为 ( O(n^2) ),因为我们需要计算 ( C[i] ) 时,内层循环需要遍历 i 次。
空间复杂度
空间复杂度为 ( O(n) ),因为我们使用了一个大小为 ( n+1 ) 的数组来存储卡特兰数。
思路三:动态规划
使用组合数学中递推思想
统计方案数目,就是最简单的动态规划。
可以按照下面的方法进行分析:
- 问题建模
问题描述:
在 2n 个算筹中放置 n 个红色算筹,满足 任意前 i 个算筹中红色算筹数 ≥ ⌈i/2⌉(即红色算筹始终不少于半数)。
DP状态定义:
dp[i][j] 表示前 i 个算筹中放置 j 个红色算筹的合法方案数。
- 动态规划转移方程
递推关系:
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
dp[i-1][j]:第 i 个算筹不选红色(即选黑色)。
dp[i-1][j-1]:第 i 个算筹选红色。
在循环里面写着一步的时候就可以加上 模 100了。
约束条件:
j ≥ ⌈i/2⌉,确保任意前缀中红色算筹数满足要求。
- 算法核心逻辑
初始化:
dp[1][1] = 1,表示第一个算筹必须为红色(因为 ⌈1/2⌉ = 1)。
递推填充DP表:
外层循环遍历算筹总数 i(从 2 到 2n)。
内层循环遍历红色算筹数 j(从 ⌈i/2⌉ 到 i)。
结果输出:
dp[2n][n] 表示 2n 个算筹中放 n 个红色算筹的合法方案数。

浙公网安备 33010602011771号