从一道NOI练习题说递推和递归
一、递推:
所谓递推,简单理解就是推导数列的通项公式。先举一个简单的例子(另一个NOI练习题,但不是这次要解的问题):
楼梯有n(100 > n > 0)阶台阶,上楼时可以一步上1阶,也可以一步上2阶,也可以一步上3阶,编程计算共有多少种不同的走法。
这个问题可以用递归来进行解决,但是解题时间1秒明显不够用。怎么办呢,可以考虑找到“规律”,然后推导公式解决问题,开始画图分析:
这是4个台阶时的全部7种走法,记作f(4)=7。现在观察右侧绿色走过的部分,1234四种情况是3个台阶时的4种走,法记作f(3)=4;56两种情况是2个台阶时的2种走法,记作f(2)=2;7是一个台阶时的1种走法,记作f(1)=1。即:
f(4)=f(3)+f(2)+f(1)。
那么,现在把台阶数增加到5。思考一下,左面再有一个台阶时是什么情况:
1、若先走3步,则剩下2个台阶,于是有先走3阶时(剩余2阶)的走法数与f(2)相等。
2、若先走2步,则剩下3个台阶,于是有先走2阶时(剩余3阶)的走法数与f(3)相等。
3、若先走1步,易推出:剩余4阶,走法数与f(4)相等。
f(5)的全部情况遍历完毕,于是有:
f(5)=f(2)+f(3)+f(4)
f(n)可以做相同推理,可以得到数列的通项公式:
当0<n<3时:
f(1)=1
f(2)=2
f(3)=4
当n>3时:
f(n)=f(n-1)+f(n-2)+f(n-3)
得到通项公式之后,这个问题就非常简单了。用递推时,程序得出结果速度要比递归快,不过需要烧掉的脑细胞比较多……
二、递归
所谓递归函数,就是在函数内部调用自己。那么,递归解决问题时,可以更好的把着眼点放在问题的相同部分。即递归能解决那些一个问题可以分解成若干个子问题,再分解成若干子问题……而且这些父问题和子问题都是相同问题的问题;就是这些儿子和儿子的儿子和儿子的儿子的儿子……子子孙孙无穷尽也……但是都和隔壁老王没关系的意思。咳咳,来看我们要解决的这个NOI练习题:
总时间限制: 2000ms 单个测试点时间限制: 1000ms 内存限制: 131072kB
描述
对于一个2行N列的走道。现在用1*2,2*2的砖去铺满。问有多少种不同的方式。 下图是一个2行17列的走道的某种铺法。
(注:上图来源于http://noi.openjudge.cn/) 输入
整个测试有多组数据,请做到文件底结束。每行给出一个数字N,0 <= n <= 250 输出
如题 样例输入
2
8
12
100
200
样例输出
3
171
2731
845100400152152934331135470251
1071292029505993517027974728227441735014801995855195223534251
先分析一下这个题目:
1、可以竖着铺1*2的砖
2、如果横着铺了一个1*2,那么下面一定是1*2
所以,情况只有3种:竖着铺1*2、横着铺2个1*2、铺2*2。
那么递归调用很容易构建:
1、若当前已铺统计完全铺完的个数。
2、若当前没有铺完:剩余1格时,只能铺1*2;其他情况还有另外两种选择——横铺1*2和铺2*2。
代码如下:
void recursive(int curlen){
if(curlen==maxlen){
addcnt();
}else{
recursive(curlen+);
if(maxlen-curlen>){
recursive(curlen+);
recursive(curlen+);
}
}
}
前面已经说明了为什么有两次相同调用:横着铺1*2和铺2*2都是向右两格。看起来万事大吉了。但是根据以前写五子棋引擎的经验,想要搜索二百五这么多的层,即使每个节点只有两个子节点也是耗时无数的,更不用说这基本都是3节点了。题目限定时间内完全不可能解完。于是,运行了一下n=100尝试,果然行一次五谷轮回之事回来之后我的4590还在忙于计算n+1=?的问题,当然,绝大部分时间都浪费在进出栈上面了。接下来,就是递归改递推来化腐朽为神奇的时刻了,稍微修改代码,让它输出n较小时的一些结果以便观察数据规律:
#include<iostream>
#include<cstring> using namespace std;
int maxlen,cnt[],cntidx=,k=;
void addcnt(){
cnt[]++;
for(int i=;i<=cntidx;i++){
if(cnt[i]>=k){
cnt[i]=cnt[i]%k;
cnt[i+]++;
}else{
break;
}
}
if(cnt[cntidx+]==){
cntidx++;
}
}
void recursive(int curlen){
if(curlen==maxlen){
addcnt();
}else{
recursive(curlen+);
if(maxlen-curlen>){
recursive(curlen+);
recursive(curlen+);
}
}
}
int main(){
for(int j=;j<;j++){
maxlen=j;
memset(cnt,,sizeof(cnt));
recursive();
printf("%2d %d",j,cnt[cntidx]);
for(int i=cntidx-;i>=;i--){
printf("%04d",cnt[i]);
}
printf("\n");
}
}
输出是这样的:
1 1
2 3
3 5
4 11
5 21
6 43
7 85
8 171
9 341
10 683
11 1365
12 2731
13 5461
14 10923
请按任意键继续. . .
哦吼,还是挺明显的,后一项都差不多是前一项*2。好吧,不能差不多,当n为偶数时:f(n)=f(n-1)*2+1,当n为奇数时:f(n)=f(n-1)*2-1。虽然没有画图(估计就我这识海这么多窟窿,画也画不明白)来分析,但是这个公式应该可信度还是很高的…………权当是它吧。
到这里问题的思路就理清了。不过问题也来了,这个通项公式里面有加、减、乘三种运算,还需要写这几个函数。最好还是写函数吧,易读易维护。感叹一下,NOI里面有一个提问功能,类似论坛,有一些热心的学生(从言行表现的蓬勃朝气看应该是)针对问题提供一些答案和题目的“坑”,不过不仅论坛本身的排版不好影响阅读,而且绝大多数编码都在一个函数里面,变量命名惜字如金,导致阅读非常困难,可能是青年学生和他们的授业者脑力好吧,不过这些都绝对是不可取的编程方式,那怕是再小的代码,除去约定俗成的ijk……这些用来少次数嵌套循环的变量外,都应该能够顾名思义。首先,来写这几个大数加减乘法,其中省略了操作数2,因为它们是固定的1、1、2,权当复习一下前几天写的大数加减法代码:
int add(int v[],int cnt){
v[]++;
int i,tmp;
for(i=;i<=cnt;i++){
tmp=v[i];
if(tmp>=k){
v[i]=tmp%k;
v[i+]+=;
}else{
break;
}
}
if(v[cnt+]==){
cnt+=;
}
return cnt;
}
int sub(int v[],int cnt){
v[]--;
int i,tmp;
for(i=;i<cnt;i++){
tmp=v[i];
if(tmp<){
v[i]=tmp+k;
v[i+]-=;
}else{
break;
}
}
if(cnt> && v[cnt]==){
cnt-=;
}
return cnt;
} int mult(int in[],int incnt,int out[]){
int i,tmp;
for(i=;i<=incnt;i++){
out[i]=in[i]*;
}
for(i=;i<=incnt;i++){
tmp=out[i];
if(tmp>=k){
out[i]=tmp%k;
out[i+]+=;
}
}
if(out[incnt+]!=){
incnt++;
}
return incnt;
}
咳咳,好像我用了保留字作为形参名。新手嘛,嘿,下次不了。其中k表示进制,使用的是万进制。加减法都是直接修改数组,乘法是另存新数组,因为要根据n-1时的值来计算n时的值保留在数组里以备输出,所以不能覆盖。这几个函数的返回值都是运算完成时做返回值用的数组的元素的最大下标。
然后就是开始循环计算了:
data[][]=;
for(i=;i<;i++){
lintlen=mult(data[i-],lintlen,data[i]);
if(k==){
lintlen=add(data[i],lintlen);
}else{
lintlen=sub(data[i],lintlen);
}
data[i][lintlenbit]=lintlen;
k*=-;
}
这里在每个表示大数的数组(n=250时用到20个int)结尾又增加了一个int,用来记录用该数组中用到的最大下标,所以初始定义了每个表示大数的数组有21个元素。另外,为了编码简单,data数组下标开始于1以对应输入的n。不过后来data[0]还真的用到了,因为这个题评分时存在一个问题:n=0时应输出1。让我们来思考一下,这是不是代表往一个长度为0的走道里铺砖呢,然后没法铺的铺法个数为1种:没法铺。这个问题在现在:2016-12-24 22:55确实存在,得分为6分:
以后会不会修改就不得而知了。可以在下面完整代码中修改data[0][0]=1;一行为data[0][0]=0;测试。另一个问题就是不知道为什么,提交后显示成功,但只有9分。另:根据这个题目提交之后运行时间只有1ms是不是可以推测,评测端当输入输出流正常相应时开始计时,而不是从进程启动开始,很明显这比较合理。有没有漏洞呢?例如用递归计算的代码来代替递推计算的代码,那么递归的时间不被计算在内,当用递归初始化完data[][]之后,同样用查询来输出答案,是不是也可以不超时呢?答案是否定的,看看我后来提交的那个89b的死循环就知道了,也许它证明了前面的猜测是错误的,只是评测端比较快:)
最后,就是这份完整的代码了:
#include<iostream>
#include<cstring>
using namespace std;
const int k=;
const int lintlenbit=; //data[250]用了19个int,20位用来保存数据长度。
int data[][lintlenbit+];
int add(int v[],int cnt){
v[]++;
int i,tmp;
for(i=;i<=cnt;i++){
tmp=v[i];
if(tmp>=k){
v[i]=tmp%k;
v[i+]+=;
}else{
break;
}
}
if(v[cnt+]==){
cnt+=;
}
return cnt;
}
int sub(int v[],int cnt){
v[]--;
int i,tmp;
for(i=;i<cnt;i++){
tmp=v[i];
if(tmp<){
v[i]=tmp+k;
v[i+]-=;
}else{
break;
}
}
if(cnt> && v[cnt]==){
cnt-=;
}
return cnt;
} int mult(int in[],int incnt,int out[]){
int i,tmp;
for(i=;i<=incnt;i++){
out[i]=in[i]*;
}
for(i=;i<=incnt;i++){
tmp=out[i];
if(tmp>=k){
out[i]=tmp%k;
out[i+]+=;
}
}
if(out[incnt+]!=){
incnt++;
}
return incnt;
} void printdata(int v[],int cnt){
printf("%d",v[cnt]);
for(int j=cnt-;j>=;j--){
printf("%04d",v[j]);
}
printf("\n");
}
int main(){
int lintlen=,n=,i,k=;
data[][]=;
data[][]=;
for(i=;i<;i++){
lintlen=mult(data[i-],lintlen,data[i]);
if(k==){
lintlen=add(data[i],lintlen);
}else{
lintlen=sub(data[i],lintlen);
}
data[i][lintlenbit]=lintlen;
k*=-;
}
while(scanf("%d",&n)!=EOF){
printdata(data[n],data[n][lintlenbit]);
}
}
从一道NOI练习题说递推和递归的更多相关文章
- NOIP2013提高问题求解T2(关于递推与递归)
同步发表于我的洛谷博客. NOIP2013提高问题求解2: 现有一只青蛙,初始时在n号荷叶上.当它某一时刻在k号荷叶上时,下一时刻将等概率地随机跳到1,2,--,k号荷叶之一上,直到跳到第1号荷叶为止 ...
- 刷题向》关于一道比较优秀的递推型DP(openjudge9275)(EASY+)
先甩出传送门:http://noi.openjudge.cn/ch0206/9275/ 这道题比较经典, 最好不要看题解!!!!! 当然,如果你执意要看我也没有办法 首先,显然的我们可以用 f [ i ...
- 【Luogu】【关卡2-12】递推与递归二分(2017年10月)
任务说明:递推,层层递进,由基础推向顶层.二分不仅可以用来查找数据,还可以确定最合适的值. P1192 台阶问题 有N级的台阶,你一开始在底部,每次可以向上迈最多K级台阶(最少1级),问到达第N级台阶 ...
- 0x02 递推与递归
[例题]CH0301 递归实现指数型枚举 #include <iostream> #include <cstdio> #include <algorithm> #i ...
- 算法技巧讲解》关于对于递推形DP的前缀和优化
这是在2016在长沙集训的第三天,一位学长讲解了“前缀和优化”这一技巧,并且他这一方法用的很6,个人觉得很有学习的必要. 这一技巧能使线性递推形DP的速度有着飞跃性的提升,从O(N2)优化到O(N)也 ...
- 题解报告:hdu 2084 数塔(递推dp)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2084 Problem Description 在讲述DP算法的时候,一个经典的例子就是数塔问题,它是这 ...
- 「学习笔记」递推 & 递归
引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...
- 再谈循环&迭代&回溯&递归&递推这些基本概念
循环:不断重复进行某一运算.操作. 迭代:不断对前一旧值运算得到新值直到达到精度.一般用于得到近似目标值,反复循环同一运算式(函数),并且总是把前一 次运算结果反代会运算式进行下一次运算 递推:从初值 ...
- (acwing蓝桥杯c++AB组)1.2 递推
1.2 递推与递归 文章目录 1.2 递推与递归 位运算相关知识补充 pair与vector相关知识补充 题目目录与网址链接 下面的讲解主要针对这道题目的题解AcWing 116. 飞行员兄弟 - A ...
随机推荐
- go:windows下用sublime Text搭建go语言开发环境
一.安装 1.安装go go安装包地址:http://pan.baidu.com/s/1hq1mrDM(进入下图中箭头所示目录中下载对应版本) * 注意go的安装路径不能包含中文 ...
- Sass安装(windows版)
Sass安装(windows版) 在 Windows 平台下安装 Ruby 需要先有 Ruby 安装包,大家可以到 Ruby 的官网(http://rubyinstaller.org/download ...
- Spring Bean后处理器以及容器后处理器【转】
Bean后处理器:即当spring容器实例化Bean实例之后进行的增强处理. 容器后处理器:对容器本身进行处理,并总是在容器实例化其他任何Bean之前读取配置文件的元数据并可能修改这些数据. 一.Be ...
- TTrayIcon用法
TTrayIcon用法 self.trycn1.Icon:=Application.Icon; Self.trycn1.Hint:=self.Caption; self.trycn1.Visible: ...
- 最详细易懂的CRC-16校验原理(附源程序)
from:http://www.openhw.org/chudonganjin/blog/12-08/230184_515e6.html 最详细易懂的CRC-16校验原理(附源程序) 1.循环校验码( ...
- 纯CSS完成tab实现5种不同切换对应内容效果
很常用的一款特效纯CSS完成tab实现5种不同切换对应内容效果 实例预览 下载地址 实例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ...
- 【BZOJ】3505: [Cqoi2014]数三角形
题意 \(n * m(1 \le n, m \le 1000)\)的网格,求顶点在格点上三角形的个数. 分析 假设\(n \le m\) \(ans = \binom{(n+1) * (m+1)}{3 ...
- dev checkbox多选
GridControl如果要支持多选,设置Options->OptionSeletion->MultiSelet为true就ok.
- Java 异常java.lang.IllegalArgumentException: Illegal group reference
当字符串方法replaceAll()中替换字符含有特殊字符$如, String test = "<StreamingNo>abc</StreamingNo>" ...
- python与正则表达式
匹配一个字符: . 任意非\n字符 [...] \d \D digit \s \S space \w \W word 匹配前一个字符的多个: * 0->> + 1->> ? 0 ...