DP入门

凑钱I

题目

有面额为1、5、10、20、50、100的钞票不限数量,给定正整数n,求刚好凑出n元所需要的最小钞票张数。

思路

根据日常经验,显然可以贪心来做,即尽可能地先选择面额大的钞票。


凑钱II

题目

问题I中钞票的面额变为1、5、11,之前的解法还成立么?

思路

n=15时,贪心的结果是5(15=11+1*4),但答案显然应该是3(15=5*3)。贪心只考虑了眼前的策略,陷入了局部最优而非全局最优。

暴力枚举所有可行解复杂度过高,n比较大时难以计算。

用f(n)来表示凑出n元所需的最少钞票数量,显然f(0)=0。
考虑选取的最后一张钞票只能是1、5、11,
三种方案分别花费f(n-1)+1、f(n-5)+1、f(n-11)+1,
显然应该选择花费最小的方案,于是我们得到状态转移公式:
f(n) =min(f(n-1)+1,f(n-5)+1,f(n-11)+1)

递归

#include<bits/stdc++.h>
using namespace std;

const int INF=0x3f3f3f3f;

int dfs(int n);

int main()
{
  int n;
  cin>>n;

  cout<<dfs(n)<<endl;

  return 0;
}

int dfs(int n)
{
  if (n==0) return 0;

  int cost=INF;
  if (n-11>=0) cost=min(cost,dfs(n-11)+1);
  if (n-5>=0) cost=min(cost,dfs(n-5)+1);
  if (n-1>=0) cost=min(cost,dfs(n-1)+1);

  return cost;
}

记忆化搜索(填表法)

#include<bits/stdc++.h>
using namespace std;

const int MAXN=(int)1e5+10;
const int INF=0x3f3f3f3f;

int dp[MAXN];

int dfs(int n);

int main()
{
  int n;
  cin>>n;

  memset(dp,-1,sizeof dp);
  dp[0]=0;

  cout<<dfs(n)<<endl;

  return 0;
}

int dfs(int n)
{
  if (dp[n]!=-1) return dp[n];

  int cost=INF;
  if (n-11>=0) cost=min(cost,dfs(n-11)+1);
  if (n-5>=0) cost=min(cost,dfs(n-5)+1);
  if (n-1>=0) cost=min(cost,dfs(n-1)+1);

  return dp[n]=cost;
}

递推(刷表法)

#include<bits/stdc++.h>
using namespace std;

const int MAXN=(int)1e5+10;

int dp[MAXN];

int main()
{
  int n;
  cin>>n;

  memset(dp,0x3f,sizeof dp);
  dp[0]=0;

  for (int i=1;i<=n;i++) {
    if (i-11>=0) dp[i]=min(dp[i],dp[i-11]+1);
    if (i-5>=0) dp[i]=min(dp[i],dp[i-5]+1);
    if (i-1>=0) dp[i]=min(dp[i],dp[i-1]+1);
  }

  cout<<dp[n]<<endl;

  return 0;
}

动态规划问题的特点

无后效性

最优子结构

重叠子问题


动态规划算法设计

设计状态

转移路径

转移方程


动态规划原理

规划类问题都是在可行解空间内,寻找最优解。
暴搜是枚举所有可行解。
贪心是选取一个特解。
DP是枚举有希望成为答案的解。
DP剪掉了不可能成为最优解的答案,缩小了可行解空间,完成了优化。


最长上升子序列(LIS)

题目

给定长度为n的序列A,求A最长的单调递增子序列(LIS)的长度。
例如{3,2,1,4,7,5,6}的LIS为{3,4,5,6},长度为4 。

思路

用f(i)表示以a[i]结尾的LIS长度,初始值f(i)=1。
考虑一个小于i的j,如果a[i]>a[j],则可以把a[i]接在以a[j]结尾的LIS后面,此时得到的LIS长度为f(j)+1,枚举所有j答案取最大值更新即可。
状态转移方程为:
f(i)=max(f(j)+1) | j<i,a[i]>a[j]

代码

#include<bits/stdc++.h>
using namespace std;

const int MAXN=(int)1e3+10;

int a[MAXN],dp[MAXN];

int main()
{
  int n;
  cin>>n;
  for (int i=1;i<=n;i++) {
    scanf("%d",a+i);
    dp[i]=1;
  }

  for (int i=1;i<=n;i++) {
    for (int j=1;j<i;j++) {
      if (a[i]>a[j]) {
        dp[i]=max(dp[i],dp[j]+1);
      }
    }
  }

  int ans=0;
  for (int i=1;i<=n;i++) {
    ans=max(ans,dp[i]);
  }

  cout<<ans<<endl;

  return 0;
}

扔鸡蛋

题目

你在一栋100层的楼上,有两个一模一样的鸡蛋。你需要从楼上往下扔鸡蛋来测试鸡蛋硬度。
如果在第 x 层扔下鸡蛋,鸡蛋不碎,那么从第 x-1 层扔鸡蛋也不碎。
如果鸡蛋从第x层扔下去没摔坏,但从第x+1层扔下去摔坏了,则鸡蛋硬度=x。
特别地,如果从第1层扔下去就坏了,则鸡蛋硬度 =0。
如果在最高的第n层扔下去还没摔坏,则鸡蛋硬度=n。
求最坏情况下的最少测试次数。

思路

设f(m,n)表示有m个鸡蛋测试n层楼时最坏情况下的最多需要次数。
只剩一个鸡蛋的时候,为了确保测试成功,只能采用最保守的方法,一层一层往上试,f(1,n)=n。

对于f(m,n),假设第一次测试在k层,有两种情况:
①鸡蛋没摔坏,则还需要测试n-k层,转化为f(m,n-k)。
②鸡蛋摔坏了不能继续用于测试,则还需要测试k-1层,转化为f(m-1,k-1)。
考虑最坏情况,测试次数取max,即
f(m,n)=max(f(m,n-k), f(m-1,k-1) )+1

枚举所有可能的k值,取最小次数即可。

代码

#include<bits/stdc++.h>
using namespace std;

const int INF=0x3f3f3f3f;
const int MAXM=50+10;
const int MAXN=(int)1e3+10;

int dp[MAXM][MAXN];

int main()
{
  int m,n;
  scanf("%d%d",&m,&n);

  memset(dp,0x3f,sizeof dp);
  for (int i=1;i<=m;i++) {
    dp[i][0]=0;
  }
  for (int i=1;i<=n;i++) {
    dp[1][i]=i;
  }

  for (int i=1;i<=m;i++) {
    for (int j=1;j<=n;j++) {
      for (int k=1;k<=j;k++) {
        dp[i][j]=min(dp[i][j],max(dp[i-1][k-1],dp[i][j-k])+1);
      }
    }
  }

  printf("%d\n",dp[m][n]);

  return 0;
}

留下评论