洛谷P1044栈 记忆化搜索/动态规划/卡特兰数

本文记录了解决一道涉及栈操作计数的算法题的心路历程,从手动尝试寻找数学规律到使用深度优先搜索、记忆化搜索和动态规划,最终发现答案与卡特兰数紧密相关。通过递推公式和卡特兰数公式,作者揭示了问题的数学本质。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目传送门

前言

很久没有写算法题了。放假拿起来写两个简单的,就看到这个题。总体来说难度不大~~(虽然写了好久)~~,但是有新的知识点——卡特兰数。值得一写。

心路历程1——尝试找出数学规律

看到他是 普通- 难度的题,然后输入输出很简单,就打算手动算一下找一找数学规律。

结果如下表

nans
11
22
35
414
542
6132

好像总有那么一点点规律。于是进行一顿乱蒙,没有找到简单的一次二次线性关系。

尝试推导递推公式,就是能感觉到有规律却写不出来简介的公式。

花了很多时间没有什么收获,但是对上面这个表格,有了很深的影响。

心路历程2——暴力深搜(DFS)

  • 思路

    想一下这个题目背景,好像有很多种情况,但是发现你能控制的只有两个操作,push进栈和pop出栈。

    拿n=3的为例,一定会有3次push和3次pop。

    也就是,总共六次操作,每次有两种选择,这就是一个6bit的二进制数。

    不仅如此,还有一个约束:出栈之前栈内必须要有东西。

    进栈当做1,出栈当做0,就可以画出一个二叉树。

    image-20220713233953455

    粉色标出的是出栈的时候栈为空,需要剪枝。

    发现画树是一个很好的理清深搜思路的方法

  • 代码

    #define _CRT_SECURE_NO_WARNINGS
    #include <bits/stdc++.h>
    using namespace std;
    #define ll long long
    
    int n;
    int sta;
    ll cnt;
    int push_num;
    
    void dfs(int x)
    {
    	if (x == n * 2 && sta == 0) {
    		cnt++;
    		return;
    	}
    	else if (x == n * 2) {
    		return;
    	}
    
    	if (push_num > 0) {
    		push_num--;
    		sta++;
    		dfs(x + 1);
    		sta--;
    		push_num++;
    	}
    
    	if (sta > 0) {
    		sta--;
    		dfs(x + 1);
    		sta++;
    	}
    }
    
    
    int main()
    {
    	scanf("%d", &n);
    	push_num = n;
    	dfs(0);
    
    	printf("%lld", cnt);
    	return 0;
    }
    
  • 结果

    很不出意外的TLE了一个点。

    发现只是慢了一点点并没有到跑不出来的地步,打一个表结束这道题(不)

心路历程3——记忆化搜索(MS)

  • 思路

    记忆化搜索是一个典型的空间换时间。

    某一些状态在深搜的过程中出现过很多次,我们把这个状态对应的结果保存下来,再以后遇到相同的结果时直接调用,就可以剩下很多时间。

    那么这里的状态怎么确定呢?

    有很多种做法,我的思路是:栈内有几个数+还能push几次,这两个值确定一个状态。

    每次进入深搜时,先查找是否已经保存了该状态:

    • 有,直接获取结果
    • 无,继续深搜,保存状态
  • 代码

    这里用了很多stl容器,map和pair。其实完全可以直接用二维数组,只是想用用stl容器。

    map可以用unordered_map(哈希表),理论上更快。

    #define _CRT_SECURE_NO_WARNINGS
    #include <bits/stdc++.h>
    using namespace std;
    #define ll long long
    
    int n;  //输入的数字,总共有几个待进栈数字
    ll cnt;
    
    int sta;       //栈内元素数量
    int push_num;  //还可以压几次
    
    map<pair<int, int>, ll> result;
    
    ll dfs(int x)
    {
    	auto it = result.find(make_pair(sta, push_num));
    	if (it != result.end())  //如果能在result中找到对应的状态
    	{
    		//加入计数器,并返回
    		ll t = it->second;
    		cnt += t;
    		return t;
    	}
    
    	if (x == 2 * n && sta == 0) {
    		cnt++;
    		return 1;
    	}
    	else if (x > 2 * n) {
    		return 0;
    	}
    
    	ll tmp = 0;
    	if (push_num > 0) {
    		push_num--;
    		sta++;
    		tmp += dfs(x + 1);
    		sta--;
    		push_num++;
    	}
    
    	if (sta > 0) {
    		sta--;
    		tmp += dfs(x + 1);
    		sta++;
    	}
    
    	result[make_pair(sta, push_num)] = tmp;
    	return tmp;
    }
    
    int main()
    {
    	scanf("%d", &n);
    	push_num = n;
    	dfs(0);
    
    	printf("%lld", cnt);
    	return 0;
    }
    
  • 结果

    轻松AC本题。n取到1000也较快能算完。n再大就要爆long long了。

心路历程4——动态规划

  • 思路

    都写到这里了,不继续来个动归吗?

    记忆化搜索和动归,感觉是完全相同的底层逻辑的两种不同表现形式。

    已经写出了记忆化搜索,最难的写出递推公式那一步就迎刃而解了。

    看上面的深搜

    	if (push_num > 0) {
    		push_num--;
    		sta++;
    		tmp += dfs(x + 1);
    		sta--;
    		push_num++;
    	}
    
    	if (sta > 0) {
    		sta--;
    		tmp += dfs(x + 1);
    		sta++;
    	}
    

    得到公式(i = sta, j = push_num)

    dp[i][j] = dp[i + 1][j - 1] + dp[i - 1][j];
    

    然后看边界条件

    栈内没有东西,还有n次可以push的情况数量,就是要求的答案。

    不能再push了(push_num=0时),就确定了一种情况

    再发现,i=0时,dp[i-1][j]没有意义,所以加一个if判断一下。

    上代码。

  • 代码

    #define _CRT_SECURE_NO_WARNINGS
    #include <bits/stdc++.h>
    using namespace std;
    #define ll long long
    
    ll dp[1005][1005];
    int main()
    {
    	int n;
    	scanf("%d", &n);
    	for (int i = 0; i <= n; i++) {
    		dp[i][0] = 1;
    	}
    
    	for (int j = 1; j < n; j++) {
    		for (int i = 0; i <= n; i++) {
    			if (i == 0)
    				dp[i][j] = dp[i + 1][j - 1];
    			else
    				dp[i][j] = dp[i + 1][j - 1] + dp[i - 1][j];
    		}
    	}
    	printf("%lld", dp[1][n - 1]);
    
    	return 0;
    }
    
  • 结果

    实际测试下来,当数字很大时,比记忆化搜索略微快一点点,毕竟维护一个哈希表比直接读取二维数组慢得多

心路历程5——返璞归正,追寻数学本质

  • 卡特兰数

    看了题解,有一个神奇的词语映入眼帘:卡特兰数。

    卡特兰数是一个数列,他的前几项是:

    1,1,2,5,14,42,132

    眼熟吗,这就是本题的答案。

    在此类和进出栈相关的问题中,经常能遇到卡特兰数。同类型问题有,括号配对,完全二叉树等等。

    心路历程2中说过,这个问题的结果可以看做是一个2n位的二进制数,其中n位是1,n位是0

    简单的排列组合知识,这样的数一共有 C 2 n n C_{2n}^{n} C2nn 个。(2n位中选n位放1,其他放0)

    下面只要算出有多少数字不符合条件(出栈的时候栈为空),与总数相减就行了。

    • 一个不符合条件的序列如:100110

    如果进栈+1,出栈-1,栈内的数可以写成:+1,-1,-1,+1,+1,-1

    看每一项的前缀和,如果前缀和小于零,就是栈空了还出栈了一次。

    +1-1-1+1+1-1
    10-1

    拿这个数列为例,把第一个位置到第一次前缀和小于零的位置取反,就变成

    -1+1+1+1+1-1

    取反的部分-1比+1多一个,取反之后整体上看,+1比-1多一个

    也就是1有n+1个,-1有n-1个。

    取反之后的数字与取反之前是一一对应的,也就是满足1有n+1位,0有n-1位的数就是不符合条件的数。

    这样的数共有 C 2 n n + 1 C_{2n}^{n+1} C2nn+1 or C 2 n n − 1 C_{2n}^{n-1} C2nn1 个。

    所以该题的答案便是:
    C 2 n n − C 2 n n + 1 C_{2n}^{n} - C_{2n}^{n+1} C2nnC2nn+1
    这便是卡特兰数的通项公式

    稍微经过化简可以得到更直接的通项公式: C 2 n n + 1 n \frac {C_{2n}^{n+1}}{n} nC2nn+1 C 2 n n n + 1 \frac {C_{2n}^{n}}{n+1} n+1C2nn

    有了通项公式,再返回去就容易推出递推公式了: f ( n ) = f ( n − 1 ) × ( 4 − 6 n + 1 ) f(n) = f(n-1) \times (4-\frac6{n+1}) f(n)=f(n1)×(4n+16)

  • 分析

    上述三个公式本质相同,最好的还是 C 2 n n − C 2 n n + 1 C_{2n}^{n} - C_{2n}^{n+1} C2nnC2nn+1 这个。

    因为如果数据过大,可以满足模的性质。

参考资料

卡特兰数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cishoon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值