例题一:
换钱的方法数
给定数组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(3,15)这个函数(前面选择了两张5元的,0张10元的或者选择了0张5元的,一张10元的),所以,对于GetNumber(3,15)这一个递归函数,前面的两个状态都能够到达,这就出现了重复计算,使得算法时间复杂度变得很高。
第一版优化-----记忆化搜索:
通过上面的分析,我们可以将算出的结果存在一个数组中,后续程序的计算如果需要某个结果直接从数组中拿就可以了,例如我们可以将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;
}