一个实际问题的各种可能的情况构成的集合常称为”状态空间“,而程序的运行则是对于状态空间的遍历,算法和数据结构则通过划分、归纳、提取、抽象来帮助提高程序遍历状态空间的效率。递推和递归就是遍历状态空间的两种基本方式。
递推与递归的宏观描述
对于一个待求解的问题,当它局限在某处边界、某个小范围或者某个特殊情况下时,其答案往往是已知的。如果能够将该解答的应用场景扩大到原问题的状态空间,并且扩展过程的每个步骤具有相似性,就可以考虑使用递推或者递归求解。
什么是递推?递归?以求阶乘n!为例。
递推:
factorial[0]=1;
for(int i=1;i<=n;i++)
factorial[i]=factorial[i-1]*i;
递归:
int factorial(int n){
if(n==0)return 1;
return factorial(n-1)*n;
}
像第一份代码,以已知的问题边界为起点向原问题正向推导的扩展方式就是递推。像第二份代码,函数自身定义自身,通过函数体实现循环,以自相似的方式重复进行的过程就被称为递归。
对于本问题来说,n!的推导路径我们已经知道了,所以直接for循环一遍就好了。但是很多时候,推导的路径难以确定,这时就需要用到递归,以原问题为起点尝试寻找把状态空间缩小到已知的问题边界的路线,再通过该路线反向回溯。
想要使用递推或者递归解决一个问题,该问题需要保证程序在每个步骤上应该面对相同种类的问题,这些问题都是原问题的一个子问题,可能仅在规模或者某些限制上有所区别,并且能够使用求解原问题的程序进行求解。
对于递归算法,有了上面的前提,可以在每次状态变化过程中执行三个操作。
- 缩小问题状态空间的规模,这意味着程序尝试寻找在”原问题“与”问题边界“之间的变换路线,并向正在探索的路线上迈出一步。
- 尝试求解缩小以后的问题,结果可能是成功,也可能是失败。
- 如果成功,即找到了缩小后的答案,那么将答案扩展到当前问题。如果失败,那么重新回到该问题,程序可能会继续寻找当前问题的其他变化路线,知道确定当前问题无法求解。
每个问题可能有多个子问题,会向下递归多次,如果求解一个子问题失败,程序需要重新回到当前问题寻找其他的路线,因此把当前问题缩小为之前的子问题时所做的“对当前问题状态产生影响的事情”应该全部失效,这被称为“还原现场”。具体来说,就是再一次递归完成后,修改过的所有全局变量应该恢复为递归开始之前的状态。
递推与递归的简单应用
枚举形式 | 状态空间规模 | 一般遍历方式 |
---|---|---|
多项式 | n k , k n^k,k nk,k为常数 | 循环(for),递推 |
指数 | k n , k k^n,k kn,k为常数 | 递归,位运算 |
排列 | n ! n! n! | 递归,C++next_permutation |
组合 | C ( n , m ) C(n,m) C(n,m) | 递归+剪枝 |
#include<iostream>
#include<vector>
using namespace std;
vector<int>chosen;
int n;
void calc(int pos)
{
if(pos==n+1)
{
for(auto k:chosen)
{
printf("%d ",k);
}
printf("\n");
return ;
}
calc(pos+1);
chosen.push_back(pos);
calc(pos+1);
chosen.pop_back();
return ;
}
int main()
{
cin>>n;
calc(1);
return 0;
}
#include<iostream>
#include<vector>
using namespace std;
vector<int>chosen;
int n,m;
void calc(int pos)
{
if(chosen.size()>m||chosen.size()+(n-pos+1)<m)return ;
if(pos==n+1)
{
for(auto k:chosen)
{
printf("%d ",k);
}
printf("\n");
return ;
}
chosen.push_back(pos);
calc(pos+1);
chosen.pop_back();
calc(pos+1);
return ;
}
int main()
{
cin>>n>>m;
calc(1);
return 0;
}
#include<iostream>
using namespace std;
int n;
int chosen[15];
int vis[15];
void calc(int x)
{
if(x==n+1)
{
for(int i=1;i<=n;i++)
printf("%d ",chosen[i]);
printf("\n");
return ;
}
for(int i=1;i<=n;i++)
{
if(vis[i])continue;
chosen[x]=i;
vis[i]=1;
calc(x+1);
vis[i]=0;
}
return ;
}
int main()
{
cin>>n;
calc(1);
return 0;
}
易发现三个性质:
- 每个位置至多会被点击一次。
- 若固定了第一行,则满足题意得点击方案至多只有一种。当第i行某一位时1,若前i行已经被固定,只能点击i+1行该位置上的数字才能使第i行这一位变成0.自上而下按行使用归纳法。
- 点击的先后次序不影响最终结果。
#include<iostream>
#include<cstring>
using namespace std;
int t;
int cnt=0,res=10;
char g[10][10],dg[10][10];
int dx[5]={-1,0,0,0,1};
int dy[5]={0,-1,0,1,0};
void turn(int x,int y)
{
for(int i=0;i<5;i++)
{
int xx=x+dx[i],yy=y+dy[i];
if(xx<1||xx>5||yy<1||yy>5)continue;
g[xx][yy]^=1;
}
return ;
}
int main()
{
cin>>t;
while(t--)
{
res=10;
for(int i=1;i<=5;i++)
for(int j=1;j<=5;j++)
cin>>dg[i][j];
for(int i=0;i<32;i++)
{
memcpy(g,dg,sizeof g);
cnt=0;
int r=i;
for(int j=1;j<=5;j++)
{
if(r&1)
{
turn(1,j);
cnt+=1;
}
r>>=1;
}
for(int j=2;j<=5;j++)
{
for(int k=1;k<=5;k++)
{
if(g[j-1][k]=='0')
{
turn(j,k);
cnt+=1;
}
}
}
int flag=1;
for(int j=1;j<=5;j++)
if(g[5][j]=='0')flag=0;
if(flag)
{
res=min(res,cnt);
}
}
if(res>6)cout<<-1<<endl;
else cout<<res<<endl;
}
return 0;
}
acwing96.奇怪的汉诺塔
对于三塔模型:
d
[
n
]
=
2
∗
d
[
n
−
1
]
+
1
d[n]=2*d[n-1]+1
d[n]=2∗d[n−1]+1
对于本问题:
f
[
n
]
=
m
i
n
(
2
∗
f
[
i
]
+
d
[
n
−
i
]
)
f[n]=min(2*f[i]+d[n-i])
f[n]=min(2∗f[i]+d[n−i])
#include<iostream>
#include<cstring>
using namespace std;
int dp1[20],dp2[20];
int main()
{
dp1[1]=1;
for(int i=2;i<=12;i++)
dp1[i]=2*dp1[i-1]+1;
memset(dp2,0x3f,sizeof dp2);
dp2[0]=0;
for(int i=1;i<=12;i++)
{
for(int j=0;j<=i;j++)
{
dp2[i]=min(dp2[i],dp2[j]*2+dp1[i-j]);
}
cout<<dp2[i]<<endl;
}
return 0;
}