《算法竞赛进阶指南》0x02递推与递归

一个实际问题的各种可能的情况构成的集合常称为”状态空间“,而程序的运行则是对于状态空间的遍历,算法和数据结构则通过划分、归纳、提取、抽象来帮助提高程序遍历状态空间的效率。递推和递归就是遍历状态空间的两种基本方式。


递推与递归的宏观描述

对于一个待求解的问题,当它局限在某处边界、某个小范围或者某个特殊情况下时,其答案往往是已知的。如果能够将该解答的应用场景扩大到原问题的状态空间,并且扩展过程的每个步骤具有相似性,就可以考虑使用递推或者递归求解。

什么是递推?递归?以求阶乘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循环一遍就好了。但是很多时候,推导的路径难以确定,这时就需要用到递归,以原问题为起点尝试寻找把状态空间缩小到已知的问题边界的路线,再通过该路线反向回溯。

想要使用递推或者递归解决一个问题,该问题需要保证程序在每个步骤上应该面对相同种类的问题,这些问题都是原问题的一个子问题,可能仅在规模或者某些限制上有所区别,并且能够使用求解原问题的程序进行求解。

对于递归算法,有了上面的前提,可以在每次状态变化过程中执行三个操作。

  1. 缩小问题状态空间的规模,这意味着程序尝试寻找在”原问题“与”问题边界“之间的变换路线,并向正在探索的路线上迈出一步。
  2. 尝试求解缩小以后的问题,结果可能是成功,也可能是失败。
  3. 如果成功,即找到了缩小后的答案,那么将答案扩展到当前问题。如果失败,那么重新回到该问题,程序可能会继续寻找当前问题的其他变化路线,知道确定当前问题无法求解。

每个问题可能有多个子问题,会向下递归多次,如果求解一个子问题失败,程序需要重新回到当前问题寻找其他的路线,因此把当前问题缩小为之前的子问题时所做的“对当前问题状态产生影响的事情”应该全部失效,这被称为“还原现场”。具体来说,就是再一次递归完成后,修改过的所有全局变量应该恢复为递归开始之前的状态。

递推与递归的简单应用

枚举形式状态空间规模一般遍历方式
多项式 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)递归+剪枝

例题
acwing92.递归实现指数型枚举

#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;
}

acwing93.递归实现组合型枚举

#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;
}

acwing94.递归实现排列型枚举

#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;
}

acwing95.费解的开关

易发现三个性质:

  1. 每个位置至多会被点击一次。
  2. 若固定了第一行,则满足题意得点击方案至多只有一种。当第i行某一位时1,若前i行已经被固定,只能点击i+1行该位置上的数字才能使第i行这一位变成0.自上而下按行使用归纳法。
  3. 点击的先后次序不影响最终结果。
#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]=2d[n1]+1
对于本问题:
f [ n ] = m i n ( 2 ∗ f [ i ] + d [ n − i ] ) f[n]=min(2*f[i]+d[n-i]) f[n]=min(2f[i]+d[ni])

#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;
}
  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值