暴力递归转动态规划-----2

例题一:

换钱的方法数


给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法。
【举例】
arr=[5,10,25,1],aim=0。
组成0元的方法有1种,就是所有面值的货币都不用。所以返回1。
arr=[5,10,25,1],aim=15。

组成15元的方法有6种,分别为3张5元、1张10元+1张5元、1张10元+5张1元、10张1元+1张5元、2张5元+5张1元和15张1元。所以返回6。
arr=[3,5],aim=2。
任何方法都无法组成2元。所以返回0。

思路:

最暴力的解法:

我们定义一个递归函数GetNumber(Aim,index),代表只能从arr数组index位置及以后的位置换钱,能够正好组成面值为Aim的方法数。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 1000001
typedef long long ll;
using namespace std;
int arr[MAXN];
int n,aim;
int GetNumber(int Aim,int index)
{
    if(index==n)          //到达了arr数组的最后的位置
        return Aim==0?1:0;//如果Aim为0,说明找到了一种换钱的方法,返回1
    int ans=0;                
    for(int i=0;i*arr[index]<=Aim;i++)//枚举arr数组当前位置所代表的钱的张数
        ans+=GetNumber(Aim-i*arr[index],index+1);
    return ans;
}
int main()
{
    cin>>n>>aim;
    for(int i=0;i<n;i++)
        cin>>arr[i];
    cout<<GetNumber(aim,0)<<endl;
}

我们来分析一下这种方法为何暴力:

我们以题目中第二组例子为例

arr=[5,10,25,1],aim=15

对于这组例子,程序肯定会进入GetNumber(5,3)这个函数(前面选择了两张5元的,0张10元的或者选择了0张5元的,一张10元的),所以,对于GetNumber(5,3)这一个递归函数,前面的两个状态都能够到达,这就出现了重复计算,使得算法时间复杂度变得很高。

第一版优化-----记忆化搜索:

通过上面的分析,我们可以将算出的结果存在一个数组中,后续程序的计算如果需要某个结果直接从数组中拿就可以了,例如我们可以将GetNumber(3,15)这个函数算出的结果存在一个数组中,如果后面的计算还需要GetNumber(3,15)这个函数算出的结果的话,就可以直接从数组中拿出来了,避免的重复计算。

代码:

​
#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 10001
typedef long long ll;
using namespace std;
int visited[MAXN][MAXN];//将计算结果存在这个数组中,横坐标代表arr数组的下标索引,纵坐标代表Aim的变化
int arr[MAXN];
int n,aim;
int GetNumber(int Aim,int index)
{
    if(visited[index][Aim]!=-1)//数组中存在GetNumber(Aim,index)这个函数的返回值,直接从数组中拿就行了
        return visited[index][Aim];
    if(index==n)
        return Aim==0?1:0;
    int ans=0;
    for(int i=0;i*arr[index]<=Aim;i++)
        ans+=GetNumber(Aim-i*arr[index],index+1);
    visited[index][Aim]=ans;//给visited数组赋值
    return ans;
}
int main()
{
    cin>>n>>aim;
    for(int i=0;i<n;i++)
        cin>>arr[i];
    memset(visited,-1,sizeof(visited));
    cout<<GetNumber(aim,0)<<endl;
}

​

第二版优化---动态规划版本:

我们可以根据暴力解法直接写出动态规划的版本,具体如何写这里就不讲了,我的另一个博客已经写了,可以去看那一个博客。

这里我们讲一下改完动态规划后还有一个优化,

通过分析我们可以得到这样一张图,图中五角星位置就是我们要求的位置,四角星位置是任意一个位置,那么通过分析可以得到,四角星位置是由它的下一行的三个心型位置的值累加得到的,而六角星位置是由它的下一行前两个心型位置的值累加得到的,所以,通过观察可以得到四角星位置可以由同行的六角星位置加上它正下方的心型位置上的值累加的到,这就是一个优化。

可以使得在写代码时两重循环里面不用在套一个while循环。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 10001
typedef long long ll;
using namespace std;
int visited[MAXN][MAXN];
int arr[MAXN];
int n,aim;
int main()
{
    cin>>n>>aim;
    for(int i=0;i<n;i++)
        cin>>arr[i];
    memset(visited,0,sizeof(visited));
    visited[n][0]=1;
    for(int i=n-1;i>=0;i--)
        for(int j=0;j<=aim;j++)
            if(j-arr[i]>=0)
                visited[i][j]=visited[i][j-arr[i]]+visited[i+1][j];//这就是我们所分析出来的状态转移方程
            else
                visited[i][j]=visited[i+1][j];//如果越界了,就只加下面那个就好
    cout<<visited[0][aim]<<endl;
}

例题二:

排成一条线的纸牌博弈问题


【题目】
给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
【举例】
arr=[1,2,100,4]。开始时玩家A只能拿走1或4。如果玩家A拿走1,则排列变为[2,100,4],接下来玩家B可以拿走2或4,然后继续轮到玩家A。如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A。玩家A作为绝顶
聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家A拿走。玩家A会获胜,分数为101。所以返回101。
arr=[1,100,2]。
开始时玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜,分数为100。所以返回100。

思路:

暴力求解思路:

这篇博客暴力求解的思路写的不错:

https://blog.csdn.net/zxzxzx0119/article/details/81274473

通过这篇博客所写的思路,我们就可以写出递归函数。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 100001
typedef long long ll;
using namespace std;
int n;
int arr[MAXN];
int End(int i,int j);
int Frist(int i,int j)//玩家作为先手在数组i位置到j位置上选择能够得到的最大分数
{
    if(i==j)//只有一张牌了,先手拿走
        return arr[i];
    return max(End(i+1,j)+arr[i],End(i,j-1)+arr[j]);//如果该玩家作为先手在i位置到j位置拿牌,那么该玩家在(i+1位置到j位置)或者在(i位置到j-1位置)上就作为了后手
}
int End(int i,int j)//玩家作为后手在数组i位置到j位置上选择能够得到的最大分数
{
    if(i==j)//只有一张牌了,后手没有机会拿了,先手拿走,所以返回零
        return 0;
    return min(Frist(i+1,j),Frist(i,j-1));//这个地方不太明白啥意思
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>arr[i];
    cout<<max(Frist(0,n-1),End(0,n-1))<<endl;
}

暴力递归转动态规划思路:

还是原先的那几个步骤,只不过这里有两个递归函数,所以我们要建立两个dp表,让这两个表相互推数,最终得到我们想要的结果。

 

观察上图,F表代表上述代码中的Frist函数,e表代表上述代码中的End函数,通过观察递归函数可以得到,表中的√位置就是我们要求的位置,表中的四角星位置就是我们的初始值位置,不需要计算就可以得到,F表中绿色五角星位置上的数的计算需要用到e表中的两个绿色五角星位置上的数,e表中的蓝色五角星位置上的数的计算需要用到F表中的两个蓝色五角星位置上的数。

需要注意循环的顺序。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 1001
typedef long long ll;
using namespace std;
int n;
int arr[MAXN];
int f[MAXN][MAXN],e[MAXN][MAXN];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>arr[i];
    memset(f,0,sizeof(f));
    memset(e,0,sizeof(e));
    for(int i=1;i<=n;i++)//给两个表赋初值
    {
        f[i][i]=arr[i];
        e[i][i]=0;
    }
    for(int i=n-1;i>=1;i--)//注意循环的顺序
        for(int j=i+1;j<=n;j++)
        {
            f[i][j]=max(e[i+1][j]+arr[i],e[i][j-1]+arr[j]);//状态转移方程
            e[i][j]=min(f[i+1][j],f[i][j-1]);

        }
    cout<<max(f[1][n],e[1][n])<<endl;
}

例题三:

题目:

初始给你N个位置,1位置到N位置,然后有一个机器人,初始停留在M位置上,然后告诉你这个机器人可以走P步,如果机器人初始位置在1位置,那么他只能往右走,如果机器人初始位置在N位置,那么他只能往左走,否则,这个机器人即能往左走,也能往右走,问你,这个机器人走P步之后停在K位置的走的方法数有多少种。

思路:

暴力递归的思路:

我们定义递归函数process(m,p)代表机器人走到m位置,还剩p步可以走的能够到达K位置的走法数。这样我们就可以写出递归函数了。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
typedef long long ll;
using namespace std;
int N,M,P,K;
// N   格子数
// M   初始的位置
// P   要走的步数
// K   最后要停留的位置
int process(int m,int p)//m代表当前走到的位置,p代表还剩余多少步可以走
{
    if(p==0) // 没有步数可以走了,看一下现在的位置是否为K,如果是,返回1.如果不是,返回0
        return m==K?1:0;
    if (m==1) // 现在的位置在最左边,只能往右走
        return process(m+1,p-1);
    else if(m==N) //现在的位置在最右边,只能往左走
        return process(m-1,p-1);
    else
        return process(m-1,p-1)+process(m+1,p-1); //普遍位置
}
int main()
{
    while (1)
    {
        cin>>N>>M>>P>>K;
        if(N<2||M<1||M>N||P<0||K<1||K>N)//这都是无效的输入
        {
            cout<<0<<endl;
            continue ;
        }
        cout<<process(M,P)<<endl;//调用递归函数
    }
}

动态规划的思路:

这个递归函数改动态规划很简单,通过分析我们可以得到下面这个图:

图中:

红色的五角星:我们需要求的位置

绿色的五角星:最左边的位置,需要它右上的绿色五角星位置上的数

蓝色的五角星:最右边的位置,需要它左上的蓝色五角星位置上的数

紫色的五角星:普遍的位置,需要它右上和左上的紫色五角星位置上的数字

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 10001
typedef long long ll;
using namespace std;
int N,M,P,K;
// N   位置的个数
// M   初始的位置
// P   要走的步数
// K   最后要停留的位置
int dp[MAXN][MAXN];//dp表
int main()
{
    cin>>N>>M>>P>>K;
    memset(dp,0,sizeof(dp));
    dp[0][K]=1;
    for(int i=1;i<=P;i++)
    {
        for(int j=1;j<=N;j++)
        {
            if(j==1)//最左边的位置
                dp[i][j]=dp[i-1][j+1];
            else if(j==N)//最右边的位置
                dp[i][j]=dp[i-1][j-1];
            else  // 普遍位置
                dp[i][j]=dp[i-1][j-1]+dp[i-1][j+1];
        }
    }
    cout<<dp[P][M]<<endl;
}

例题四:

字符串匹配问题
【题目】
给定字符串str,其中绝对不含有字符'-'和'*'。再给定字符串exp,其中可以含有'-'或'*','*'字符不能是exp的首字符,并且任意两个
'*'字符不相邻。exp中的'-'代表任何一个字符,exp中的'*'表示'*' 的前一个字符可以有0个或者多个。请写一个函数,判断str是否能被exp匹配。

【举例】
str="abc",exp="abc",返回true。
str="abc",exp="a-c",exp中单个'-'可以代表任意字符,所以返回true。
str="abcd",exp="-*"。exp中'*'的前一个字符是'-',所以可表示任意数量的'-'字符,当exp是"----"时与"abcd"匹配,返回true。
str="",exp="--*"。exp中'*'的前一个字符是'-',可表示任意数量的'-'字符,但是"-*"之前还有一个'-'字符,该字符不受'*'的影响,
所以str起码有一个字符才能被exp匹配。所以返回false。

思路:

暴力递归:

我们定义一个process(i,j)函数,代表从str字符串的i位置,从exce字符串的j位置开始往后匹配,如果匹配成功,返回true,如果匹配不成功,返回false。

遇到的情况一共有两大种。

情况一:

exce[j+1]位置不为 ' * ':只有当以下几个条件满足时,process(i,j)才返回true。

                      条件一:i!= str.size()

                      (如果这个条件不满足,说明str字符串到头了,exce字符串没有到头)

                      条件二:( str [ i ] == exce[ j ] )| | ( exce[ j ] == ' - ' )

                      (因为‘-’可以代表任意字符,所以这两个条件等价)

                      条件三:process ( i+1 , j+1 ) == true

                      (仔细想一下process函数的定义。这个条件加上面两个条件process函数就能够返回True)

情况二:

exce[j+1]位置为 ' * ' :如果process(i,j)能够返回true,也必须满足以下条件:

 

如果是上面这种情况,我们依次执行process(i,j+2)函数,process(i+1,j+2),process(i+2,j+2),

process(i1,j+2),如果这几个递归函数中只要有一个函数返回true,那么process(i,j)函数就返回true,否则,就执行process(i2,j+2)这个函数的返回值是啥,process(i,j)的返回值就是啥

如果是这种情况,我们直接执行process(i,j+2)函数,这个函数的返回值是啥,process(i,j)函数的返回值就是啥

代码:

代码写起来不是那么简单的,有很多细节需要自己coding

#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
typedef long long ll;
using namespace std;
string str,exce;
bool process(int i,int j)
{
    if(j==exce.size())//j已经到达了exce的最后位置,此时j位置无意义
        return i==str.size();//看看i是否已经到达了str的最后位置,如果到了,返回true,否则,返回false
    //能够执行下面的if语句的潜台词,exce字符串的j位置有字符
    if(j+1==exce.size()||exce[j+1]!='*')//条件一如果满足,说明j位置是exce字符串的最后一个字符的位置,条件二如果满足,说明exce的j+1位置上有字符,并且不是'*'
        return i!=str.size()&&(str[i]==exce[j]||exce[j]=='.')&&process(i+1,j+1);//只有这些条件都满足的情况下,才会返回true
    //能够执行下面的while语句的潜台词就是exec字符串的j+1位置有字符,并且是'*'
    while (i!=str.size()&&(str[i]==exce[j]||exce[j]=='.'))
    {
        if(process(i,j+2))//找到了一个满足条件的,直接返回true,无需再判断下面的情况了
            return true;
        i++;
    }
    return process(i,j+2);
}
int main()
{
    cin>>str>>exce;
    cout<<process(0,0)<<endl;
}

动态规划:

还是原来的步骤,通过观察递归函数,可以得到下面的图:

表中的最后一列是初始位置,红色五角星的位置为我们所求的位置,蓝色五角星位置为任意位置,这个位置需要它右下角的绿色五角星位置和他同行的右边的绿色五角星所在的那一列下面的位置。

但是通过观察,仅仅有最后一列的初始位置是不够的,比如说倒数第二列就没法计算,最后一行也没法计算。所以我们需要求出更多的初始位置才行。

表中的倒数第二列所代表的意思是exce字符串的最后一个字符和str字符串的任意位置上的字符开始匹配。

表中紫色五角星位置的情况是:

如果str[i]等于exce[j],那么说明匹配成功了,dp[i][j]就为true,否则,dp[i][j]就为false。

表中倒数第二列其它位置的情况:

此时匹配肯定不成功,因为str字符串i位置后面还有字符,exce字符串j位置后面没有字符了。

 

至此,我们又多求出来了一些初始位置,但是还是不够,我们还得求。

表中黑色五角星所在的行代表的意思是:str字符串已经到了最后了,exce字符串还没有到达最后位置。

我们可以分出以下几种情况:

情况一:

如果是这种情况,表中最后一行的值为:

情况二:

如果是这种情况,那么表中最后一行的值为:

至此,我们已经又多求出来了一些初始位置了,所以我们可以推表中的普遍位置了。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include <sstream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 1001
typedef long long ll;
using namespace std;
string str,exce;
bool dp[MAXN][MAXN];
bool IsParadigm(int i)
{
    if(exce[i]=='*')
        return false;
    while (i<exce.size())
    {
        if(i+1!=exce.size())
        {
            if(((exce[i]>='a'&&exce[i]<='z')&&exce[i+1]!='*')||(exce[i]=='*'&&(exce[i+1]<'a'&&exce[i+1]>'z')))
                return false;
        }
        else
            if(exce[i]!='*')
                return false;
        i++;
    }
    return true;
}
void Intialize()
{
    for(int i=0;i<=str.size();i++)
        dp[i][exce.size()]=(i==str.size())?true:false;
    for(int i=0;i<=str.size();i++)
    {
        if(i==str.size()-1)
            dp[i][exce.size()-1]=(exce[exce.size()-1]==str[i])?true:false;
        else
            dp[i][exce.size()-1]=false;
    }
    for(int j=0;j<exce.size()-1;j++)
        dp[str.size()][j]=IsParadigm(j)?true:false;
}
int main()
{
    cin>>str>>exce;
    Intialize();
    for(int i=str.size()-1;i>=0;i--)
    {
        for(int j=exce.size()-2;j>=0;j--)
        {
            if(exce[j+1]!='*')
                dp[i][j]=(str[i]==exce[j]||exce[j]=='.')&&dp[i+1][j+1];
            else
            {
                int i1=i;
                while(i1!=str.size()&&(str[i1]==exce[j]||exce[j]=='.'))
                {
                    if(dp[i1][j+2])
                    {
                        dp[i][j]=true;
                        break ;
                    }
                    i1++;
                }
                if(!dp[i][j])
                    dp[i][j]=dp[i1][j+2];
            }
        }
    }
    cout<<dp[0][0]<<endl;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
暴力法是一种简单但效率较低的求解0-1背包问题的方法。它通过穷举所有可能的组合来找到最优解。具体步骤如下: 1. 遍历所有可能的组合: - 对于每个物品,可以选择将其放入背包或不放入背包。 - 使用递归或循环来生成所有可能的组合。 2. 计算每个组合的总价值和总重量: - 对于每个组合,计算其总价值和总重量。 - 如果总重量超过背包的容量,则该组合无效。 3. 找到最优解: - 在所有有效的组合中,找到总价值最大的组合。 - 如果有多个组合具有相同的总价值,选择总重量最小的组合。 下面是一个使用暴力法求解0-1背包问题的Python示例代码: ```python def brute_force_knapsack(weights, values, capacity): n = len(weights) max_value = 0 best_combination = [] # 生成所有可能的组合 for i in range(2**n): combination = [] total_weight = 0 total_value = 0 # 将物品放入或不放入背包 for j in range(n): if (i >> j) & 1: combination.append(j) total_weight += weights[j] total_value += values[j] # 检查组合是否有效 if total_weight <= capacity and total_value > max_value: max_value = total_value best_combination = combination return max_value, best_combination # 示例用法 weights = [2, 3, 4, 5] values = [3, 4, 5, 6] capacity = 8 max_value, best_combination = brute_force_knapsack(weights, values, capacity) print("Max value:", max_value) print("Best combination:", best_combination) ``` 这段代码使用了两个列表`weights`和`values`来表示物品的重量和价值,`capacity`表示背包的容量。函数`brute_force_knapsack`通过遍历所有可能的组合来求解0-1背包问题,并返回最优解的总价值和最优解的物品组合。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值