01背包问题,简易AC代码加详细讲解,地宫寻宝,波动数列等DP问题。

01背包问题

题目描述

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值

输出格式

输出一个整数,表示最大价值。

两种方式解决这道题,一维和二位

二维方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9ktruph-1637338254763)(C:\Users\ASUS\Pictures\博客图片\20190810165740701.png)]

#include<iostream>
using namespace std;
int v[1000],w[1000],f[1000][1000];
//f[][]就像地图一样来存每次的最优数据
int main()
{
    int N,V;
    cin>>N>>V;
    //从1开始以防超界
    for(int i=1;i<=N;i++)
        cin>>v[i]>>w[i];
    for(int i=1;i<=N;i++)
    {
        for(int j=1;j<=V;j++)
        {
 //f[i][j]有两种可能,如果当前判断的这个物品装不进去
  //那么f[i][j]就取它上一行的值。
 //如果能装进去,就用max来进行一个选择
           
            f[i][j]=f[i-1][j];
            if(j>=v[i])
                f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
          //在这里我们可以看到f[i-1][j-v[i]]+w[i],可能大家在这个地方理解不了这部操作,我们将它拆分开,其实意思就是:[i-1]对应图中的上一行,而[j-v[i]]的意思是背包的总容量减去当前这个物品还剩下的容量最多能装多大价值的物品,可以通过图片来观察,最后再加w[i],这也为下边的一维的解法做了铺垫。
        }
    }
    cout<<f[N][V];//按照这种方式推理下去一定是二维数组的最右下角的值是最大值。
    
    system("pause");
    return 0;
}

一维方式:

二维跟一维有一点不一样,就是二维是从后边往前推的,为什么可以这样做,你可以想象一下二维的解法中,最上边和最左边都是0,第二行除了第一个数字是零,其他的第二行的数字都是一样的,所以一维可以从后往前。

再说一下为什么要从后往前, 因为如果从前往后就会出现覆盖上一次数据的现象。所以必须要从后往前推理。

#include <iostream>
using namespace std;
int v[1000],w[1000],f[1000];
int main()
{
    int N,V;
    cin>>N>>V;
    for(int i=1;i<=N;i++)
        cin>>v[i]>>w[i];
    for(int i=1;i<=N;i++)
        for(int j=V;j>=v[i];j--)
            //这里解释一下为什么j>=v[i],因为如果j<v[i]的时候,这个物品是装不进去的,没装进去说明最大值还是装到上一次的时候,所以j<[i]的时候值是没有变化的,就不需要考虑。
        {
            f[j]=f[j]//这一步可以忽略,跟二维一个道理
                f[j]=max(f[j].f[j-v[i]]+w[i]);
        }
    cout<<f[V];//为什么这里是f[V],因为按照这种思想,一定是最后一位数字是最大的!
    system("pause");
    return 0;
}

摘花生问题

关键思想就是,题目要求每次只能向下或者向右走,那么我们就比较向下和向右走的最优解。这里大家可能会向如果整体趋势是右下方向,那么左下的值不就被忽略掉了吗?,其实不是这样的,因为这个二维数组会遍历出每一个地方的最优解,下面请看核心代码。

 for(int i=1;i<=r;i++)
        {
            for(int j=1;j<=c;j++)
            {
                map[i][j]+=max(map[i-1][j],map[i][j-1]);
            }
        }

让我们来解析一下这段代码,两层for循环保证循环检查了需要的地图的所有区域,然后每一次都判断,究竟是向右还是向下走,当前位置的值能够最大,’+='的意思是需要加上它本身的值

其实这段核心代码还应该考虑第一行和第一列的特殊情况,因为它们都为0,但是没必要花那个时间去讨论,直接用这行代码更加简单明了。

接下来就是完整代码。

#include <iostream>
using namespace std;
int map[1000][1000];
int main()
{
    int N;
    cin>>N;
    while(N--)
    {
       	int r,c;
        cin>>r>>c;
        for(int i=1;i<=r;i++)
        {
            for(int j=1;j<=c;j++)
            {
                cin>>map[i][j];
            }
        }
        for(int i=1;i<=r;i++)
        {
            for(int j=1;j<=c;j++)
            {
                map[i][j]+=max(map[i-1][j],map[i][j-1]);
                //这段代码的主要作用上边已经说明了。
            }
        }
        cout<<map[r][c];
        //一定是右下角也就是边界值是最大的。
        cout<<endl;
    }
    
    system("pause");
    return 0;
}

最长子序列

先举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列

前1个数 d(1)=1 子序列为2;

前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7

前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1

前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5

前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6

前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4

前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3

前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8

前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9

#include <iostream>
using namespace std;
int a[1000];//存入子序列每一个数字的值。
int f[1000];//记录当前数字满足条件的最大子序列
int main()
{
    int N;
    int ans=0;
    cin>>N;
    for(int i=1;i<=N;i++)
    {
        cin>>a[i];
        f[i]=1;
    } 
    for(int i=1;i<=N;i++)
    {
        for(int j=1;j<i;j++)
        {
            if(a[j]<a[i])
            f[i]=max(f[i],f[j]+1);
        }
    }
    //上边的这两个for循环为本题的核心代码:
    //通过看本题上的举例相信大家已经有一点自己的想法了。第一个for循环目的是检查每一个序列中的数字,第二个for循环是为了循环当前第i个数前面的所有数。
    //f[i]=max(f[i],f[j]+1)的作用就是为了筛选出当前数的满足条件的最大子序列。
      for(int i=1;i<=N;i++)
         ans=max(ans,f[i]) ;
    //这个for循环还是非常重要,因为它不像上边两道题,最大的值都在最后,所以需要循环一下整个f[]数组来找到最大值。
                     cout<<ans;
    system("pause");
    return 0;
}

蚂蚁感冒问题

本题思路:

这道题的需要明白一个道理就是两只蚂蚁相遇就会各自反向,把这个过程理解为两只蚂蚁穿过了对方,这样理解对本题有一定帮助。

一般情况,题目说了第一个数据为感冒的蚂蚁,如果是在感冒蚂蚁左边的蚂蚁,并且向着这只感冒蚂蚁方向的蚂蚁一定会被感染,同理,这只感冒的蚂蚁右边的蚂蚁,只要是向着这只感冒蚂蚁走的都会被感染。

特殊情况:这只感冒的蚂蚁(即第一个数据,身处边界,比如在最左边,并且它行走的方向也是左边,那么它右边的蚂蚁即使是向着它的方向走(此时是左)也不会被感染,右边同理。

#include <iostream>
#include <cmath>
using namespace std;
int a[1000];
int main()
{
    int N;
    int left=0,right=0;
    cin>>N;
    for(int i=0;i<N;i++)
    {
        cin>>a[i];
    }
    for(int i=1;i<N;i++)//(0是拿来判断的,不用考虑)
    {
        if(fabs(a[i])<fabs(a[0]))
      //这里一定要两个都用fabs取绝对值,第一次写的时候忽略了fabs(a[0]),后来发现,第一个数据也可能是负值。所以必须是绝对值来进行判断。
        {  if(a[i]>0)
       //如果fabs(a[i])<fabs(a[0])说明这只蚂蚁一定在感冒的蚂蚁的左边,接着判断这只蚂蚁的行走方向。下边同理。
              left++;
        }
        else
            if(a[i]<0)
                right++;
    }
    cout<<right+left+1;
    system("pause");
    return 0;
}

买不到的数目

思路:

这道题有一个要点就是要找到一个上限值,上限值是输入的两个数字的最小公倍数。题目给的样例是4 和 7,那么上限值就是28;

#include <iostream>
using namespace std;
int main()
{
    int n,m,h;
    int flag=0;
    cin>>n>>m;
      int Max=m*n;
    for(int k=Max;k>=1;k--)
    {
       	for(int i=0;i<Max;i++)
    	{
      	  for(int j=0;j<Max;j++)
       		 {
          		  if(i*n+j*m==i)
              		  flag=1;
       		 }
    	}
     if(flag==0)
        {
           h=k;
           break;
        }
        flag=0;
        
    }
    cout<<h;
    system("pause");
    return 0;
}

地宫寻宝 (用了两种接解题思路,大致相同,看哪种方便你理解)

解题思路:

题目说了如果遇到比当前最大价值还要大的物品(所以当前最大价值的物品一定要用一个变量来进行记录,方便后续每次的比较),可拿可不拿(所以有两种情况,拿或者不拿);这道题采用递归的方式再好不过了,其实可以把递归的方法理解为能把地图上的每一可能的坐标(每一种个情况)都考虑进来然后进行一个比较。下边直接上代码。

#include <iostream>
using namespace  std;
int n,m,k,Count;		//Count用来记录当前满足条件的次数,也是最后输出的值。
int a[1000][1000];			
void dfs(int i, int j, int p, int cnt)		//p代表所有物品中的最大值,cnt代表当前手中的物品数。
{
	if (i == n - 1 && j == m - 1)		//不管通过怎样的路径,直到走到最右下角才能进行结果的判断。
	{
		if (cnt == k || cnt == k - 1 && a[i][j] > p)	
            //走到尽头后有两种可能性的判断
            //1:手中的物品数已经道道题目要求的k。
            //2:手中的物品只有k-1,但是最后一个格子的宝藏价值大于目前最大价值p,也满足条件。
			Count++; 
        //return; 这里要不要这句都行,因为下边有强制的边界限制if语句。
	}
	if (i < n)//向下走
	{
		if (a[i][j] > p)	//向下走有两种情况,即使宝藏比当前最大值还大,可以选择拿或不拿。
			dfs(i + 1, j, a[i][j], cnt + 1);		//这种是拿的情况
		dfs(i + 1, j, p, cnt);	
    //这条语句是不管满不满足条件都不拿的情况,所以这条语句一定不能在if语句内,不能加{};如果加了就变成了只有满足条件的时候才不拿的情况,而忽略了不满足条件的时候。
	}
	if (j < m)//向右走 .  跟上边同理。
	{
		if (a[i][j] > p)
			dfs(i, j + 1, a[i][j], cnt + 1);
		dfs(i, j + 1, p, cnt);
	}
}
int main()
{
	cin >> n >> m >> k;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			cin >> a[i][j];
		}
	}
	dfs(0, 0, -1, 0);//这里也要注意一下这里的-1,因为宝藏的价值又可能是0,必须从-1开始,不然地图无法考虑到每一种情况。
	cout << Count;
	system("pause");
	return 0;
}

下边是第二种理解方式,大同小异。

#include <iostream>
using namespace  std;
int n, m, k, Count;
int a[1000][1000];
void dfs(int i, int j, int p, int cnt) p代表所有物品中的最大值,cnt代表当前手中的物品数。
{
	if (i >= n || j >= m)			//跟上边的判断条件不同,超出边界就return;
		return;
	if (i == n - 1 && j == m - 1)	//一样要走到最右下角才进行判断。
	{
		if (cnt == k || cnt == k - 1 && a[i][j] > p)
			Count++;
		return;
	}

	if (a[i][j] > p)  //这里跟上边的思想其实是一样的,我们的目的就是为了把地图上的每一种情况都考虑进来,因为是采用的递归的方式,所以一定能够满足每一个坐标情况都能够被考虑。
        //可以这么想,在if语句里的这两句dfs是满足条件下行走。(即是能捡宝藏就捡)
	{
		dfs(i + 1, j, a[i][j], cnt + 1);
		dfs(i, j + 1, a[i][j], cnt + 1);
	}
    //而这两句dfs是忽略地图上的一切宝物,只管行走
	dfs(i + 1, j, p, cnt);
    dfs(i, j + 1, p, cnt);
    //这四句dfs一定能够遍历出每一种情况。(递归记忆化遍历)
}
int main()
{
	cin >> n >> m >> k;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			cin >> a[i][j];
		}
	}
	dfs(0, 0, -1, 0);			
	cout << Count;
	system("pause");
	return 0;
}

波动数列

这道题真的是需要自己花时间来理解细节。

思路:

如下图所示,该序列的首项是x,公差为set,set={a,-b};

通过下边的第一幅图我们可以知道,set的数量为n*(n-1)/2 (公式:首项加末项除以2)

我们令cnt=n*(n-1)/2 (也就是set的数量) 通过这些分析我们可以知道满足这么一个条件:序列总和=Sn=nx+cntset

我们假设a的数量为num1,因为a,b数量的总和是cnt,则b的数量就是cnt-num1; 可推出一个表达式nx+a * num1-b(cnt-num1)=s;转换一下位置也就是n * x=s-anum1+b(cnt-num1);。我们上边已经说了set的最大数量为n(n-1)/2,set有a和b两种可能,假若全是a,也就是最多有n*(n-1)/2个a(此时0个b),知道这个公式后就好办了因为公式里只有两个未知数x和num1,并且我们说了num1(也就是a的数量)上限是n*(n-1)/2 ,我们只需要通过枚举对这n*(n-1)/2 进行一次for循环,找出满足条件的值即可,知道了满足条件的num1后自然也就知道了x的值。讲到这里后,我们介绍一下下边这段代码,我直接把注释打在代码中。

    int ans=0;//用来记录满足条件的次数。
	int cnt=n*(n-1)/2;		//a和b数量的总和。
    for(int i=0;i<=cnt;i++)	//这里其实就是在从0开始枚举a的所有可能的数量,只要满足x是个整数,则说明此时这个数量的a是符合条件的。
    {
        long long h=s-i*a+(cnt-i)*b;
        //首先需要知道这段代码的意思,结合上边的公式可以知道这里的h代表的就算n*x; 这也就是为什么会有下边if判断的原因
        if(h%n==0)//这个判断的意思也就是想表达,只要求出的x是一个整数,那么此时的a的数量就是满足条件的。
            ans+=f[i];//这里的f[i]我会在下边介绍,反正只需要知道此时这个for循环中i的数量就是a的数量,f[i]则表明当前数量的a满足的组合,可能大家看到这里看不懂我说的是什么组合了,我会在下边介绍
    }

序号 对应值
0 x
1 x+set
2 x+set+set
n-1 x+(n-1)*set

情况1:

序号 对应值
1 x
2 x+a
3 x+a-b
4 x+a-b+a
5 x+a-b+a+a
总和 5x+7a-3b

a的数量是7,是由4+2+1组成的7

情况2:

序号 对应值
1 x
2 x+a
3 x+a+a
4 x+a+a-b
5 x+a+a-b-b
总和 5x+7a-3b

a的数量是7,却是由4+3组成的,对应的b的数量为3,是由2+1组成的。

大家请看这两种情况,虽然都是7个a,但是不是他们有不同的排列的可能,最重要的细节就在这里了,尽管我们通过那边那个步骤能够求出a的数量,但是a的组合方式还不知道,所有问题转变到求a的组合方式的可能性,上代码。

#include <iostream>
using namespace std;
long long n,s,a,b;
int f[100];//它的下标是a的个数,假设a=7,f[7]就代表当a个数是7的时候,可以有多少种可能来组成这个数字。可以是4+3,也可以是4+2+1,就像上边介绍的那两种情况一样。

//再讲下边这个函数的实现的时候,可能有同学会看不懂,它跟01背包问题的思路很相似,都用到了滑动数组的思想,可以看一下我上边对01背包问题的讲解,如果掌握了,相信对你的编程能力会有一定的帮助。

//下边这个动态规划是直接将二维的思想转化成一维来实现的。如果有不懂的可以去看下我01背包问题的讲解。
//我们先模拟用二维的方式来进行讲解。就用二维数组f[i][j]吧,下边的这种方式是直接将二维数组f[][]转化成了一维数组f[]。
//在正式开始讲前,以防大家混淆了二维数组f[][]的作用,这里说明一下,它跟求a的数量没有任何关系,是在a的数量求出来后,通过这个数组来分析a的数量个数可以有哪些组合方式,比如上边说到的当a个数是7的时候,可以有多少种可能来组成这个数字。可以是4+3,也可以是4+2+1。
//好了,接着正式说明f[i][j]中i和j代表的含义,i代表的是当前a的可能数,一定记得只是a,跟b没关系(可以是1,2,3,4...),它是从1开始的,j代表的是前i个数的数值总和。
//首先说明一点f[i][0]的值都必须先设为1。这里很细节,下边我举几个例子就明白了。
//   f[i][j]=f[i-1][j]  ,这是当j<i的时候。
//	f[i][j]=f[i-1][j]+f[i-1][j-i],这是当j>=2的时候。
//我再跟大家细致的讲解一下i的真正含义,比如说f[3][2],它的意思就是在数字1,2,3中挑任意数组能组成最终和(j),这里对应的就和(j)就是2,能组成2,只有一种情况就是直接选2出来,所有f[3][2]=1(此时请注意f[3][2]中是j<i的情况,用的公式是f[i][j]=f[i-1][j]。在举例,比如f[4][2],意思就是在1,2,3,4这四个数字中选任意数字来得到2,只有一种可能,即是直接选2,所以f[4][2]=1,(此时请注意f[4][2]中是j<i的情况,用的公式是f[i][j]=f[i-1][j]。而f[3][3],代表的意思是从1,2,3这三个数字中选任意数字能组成和(j),这里对应的和(j)就是3,要组成3,有两种方式,选1和2,或者直接选3,所以f[3][3]=2,(此时请注意f[3][3]中是j>=i的情况,所以用的公式是f[i][j]=f[i-1][j]+f[i-1][j-i]),我们带入具体数字进去就是f[3][3]=f[2][3]+f[2][0],请注意看这里是不是出现了f[3][0],也就是上边说到的f[i][0]的情况,这也就是为什么要把f[i][0]都设置为1,因为此时j=i,所以出现了f[2][0],我们深度刨析一下f[3][3]=f[2][3]+f[2][0]的意思,左边部分f[3][3]需要在右边部分(也就是上一次的状态上)进行叠加计算,这句表达式的f[2][3]代表要在1,2这两个数字中找到能组合出3的情况,就是1+2,也就是1,2两个数字都要选。而f[2][0]这部分的意思则是此时左边已经是f[3][3],从原来的1,2两个数中选,变到了现在的从1,2,3中选,并且选出了3(重点理解),所以f[2][0]的作用是判断,已经选了3后(重点理解),再从1,2这两个数字中选出满足0的情况,这也解释了为什么要把所有的f[i][0]都设置为1。    好了我只能讲到这个程度了,至于下边的如何将二维f[i][j]转化为一维f[i]请参考我讲到的01背包问题,强烈建议大家看懂这两道题,对你理解动态规划有很大的帮助!!
void dp(int x)		
{
    f[0]=1;
    for(int i=1;i<x;i++)	//简单说一下为什么是i<x,而不是i<=x,请看下表,a是从序号2才开始出现的,自己好好理解一下。

//| 序号 | 对应值    |
//| :--: | :-------- |
//|  1   | x         |
//|  2   | x+a       |
//|  3   | x+a+a     |
//|  4   | x+a+a-b   |
//|  5   | x+a+a-b-b |
//| 总和 | 5x+7a-3b  |
    {
        for(int j=i*(1+i)/2;j>=i;j--)
        {
            f[j]=f[j]+f[j-i];
        }
    }
}

完整代码:

#include <iostream>
using namespace std;
long long n,s,a,b;
int f[100];
void dp(int x)		
{
    f[0]=1;
    for(int i=1;i<x;i++)
    {
        for(int j=i*(1+i)/2;j>=i;j--)
        {
            f[j]=f[j]+f[j-i];
        }
    }
}
int main()
{
   cin>>n>>s>>a>>b;
    dp[n];
    int ans=0;
    int num=n*(n-1)/2;
    for(int i=0;i<=num;i++)
    {
        long long h=s-i*a+(num-i)*b;
        if(h%n==0)
            ans+=f[i];
    }
    cout<<ans;
    system("pause");
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_51721904/article/details/121433414
今日推荐