动态规划-背包问题(1)


  刚开始听到背包问题时就感觉很熟悉,当老师举出基本的背包问题时就瞬间想起来贪心部分的背包问题,实在没有想明白到底区别在哪。于是我就去搜了一下动态规划这部分的背包问题是否能用贪心的方法解决,答案都是不能,而且都举出了直接明了的例子来证明确实不能,但是都没有具体解释一下为什么不可以或者说这两种背包问题间的区别。所以我又回去看了一下贪心部分的背包问题,仔细琢磨了一下才明白了这其中的不同之处。
  其实,背包问题也就是这类问题的统称,这其中有部分可以用贪心来做有部分不可以,有部分可以用动态规划的方法解出同样也还有别的方法,不过在这里,就只研究动态规划的解法了。
  1.01背包
  正好01背包和贪心算法解决的背包问题最像,所以就在这做一下比较吧。
  在这里插入图片描述
  这个是基础的01背包的描述。
  在这里插入图片描述
  这个是以前用贪心算法求解的背包问题。
  基本一样,除了原始的背包问题多的一句话“物品可以被切开”,这一句话作用可是很关键啊。这一句话的直接作用就是,在所有物品重量总和超过背包的情况下,背包一定能够装满。而对于01背包,在再装入最后一件物品a[n]之前的n-1件物品,选择放入背包的物品可以按照贪心的规则选择,但这最后一件物品的选择就不见得还会遵循某种既定的贪心准则了。这时01背包就不再以某种贪心准则往背包里放东西了,而是用现在动态规划的方法解决。
  基本思路就是以dp[i][j]表示前i件物品放入容量为j的背包中能够得到的最大价值。则对于第i件物品就有两种可能,装与不装,同时对应两种结果。如果不装,dp[i][j]=dp[i-1][j],如果装,则dp[i][j]=dp[i-1][j-a[i]]+v[i],综合来看状态转移方程就是:
  dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i]]+v[i]);
  不过这还没完。因为在题目求解过程中能够用到的只有i和i-1两行,而前面的数据对于当前状态的求解没有关系,所以可以运用到先前滚动数组的知识去优化。但这还不够,对于当前行来说,对于dp[i][j]状态的求解只需要用到dp[i][1-j]就可以了,所以我们可以在更新数组元素时保证不破坏到需要用到的元素(即左边的元素)而求解当前值,结果就是,dp数组可以压缩成一维而只需要从最右边开始更新数组元素即可。所以j的取值就需要从大到小:
  dp[j]=max(dp[j],dp[j-a[i]]+v[i]);
  要注意的是,虽然数组被降成了一维的,但不能减少计算次数,只是这时这个一维dp数组枚举的元素前后将代表不同的含义(即i与i-1的变化)。不过时间复杂度也应该能够降低,因为数组大小也影响时间复杂度嘛。
  2.完全背包
  在这里插入图片描述
  这是基础的完全背包的描述。
  和01背包区别只是:不再是有n件物品而是有n种物品,每种物品有无限件。则此时dp[i][j]的含义就是前i中物品装入容量为j的背包中能够得到的最大价值。所以,对于第i种物品装多少件才能够得到最大价值就需要讨论一下。因此得到的状态转移方程就是:
  dp[i][j]=max(dp[i-1][j-ka[i]]+kv[i]),(0<=k<=V/a[i]);
  类似01背包的情况,只用到了i和i-1的状态,所以可以做第一步优化(滚动数组),但无论如何,第三重k循环避免不了。具体方法就不再重复了,总之得出来的消去k的状态转移方程就是:
  dp[i][j]=max(dp[i-1][j],dp[i][j-a[i]]*v[i]);
  当得到这一状态转移方程时,再对比01背包的状态转移方程就能够明白,这一方法其实是当放入第i种物品时,如果不装就和i-1种状态相同,而如果装,那么就一件一件的装,在装入某一件时,与装入第i种物品的前一件状态有关。对于装入时来讲,状态j-a[i]就是装入这一件前的状态(所以第一件前就是0件)正好可以实现第i种物品放多少件的状态与第i-1种物品放多少件状态间的衔接,如此就实现了消k。不过还没完,既然在原先k=0处可以实现前i-1状态和第i种之间的衔接,那么衔接之后前i-1种状态的数据就没用了(因为再之后的j变化的状态都是与k=0状态有关),所以状态转移方程可以继续优化:
  dp[j]=max(dp[j],dp[j-a[i]]+v[i]);
  不过,因为这次在第i件物品放入新的一件时状态与第i中物品放入这件之前的状态有关(包括0件),即用到的数据是由i-1到i更新以后的数据,所以枚举j时需要从左往右进行枚举,这就是完全背包与01背包的区别。这样,完全背包的问题就解决了。
  本来打算背包问题就一次性结束的,不过忽然想起来今晚cf还有比赛,所以就把背包问题分成两次吧,而正好也准备整理下cf做的题目,想着专门建立一个专栏挺麻烦的,所以干脆就放这吧。
  B. Nastya and Door
time limit per test1 second
memory limit per test256 megabytes
inputstandard input
outputstandard output
On February 14 Denis decided to give Valentine to Nastya and did not come up with anything better than to draw a huge red heart on the door of the length k (k≥3). Nastya was very confused by this present, so she decided to break the door, throwing it on the mountains.

Mountains are described by a sequence of heights a1,a2,…,an in order from left to right (k≤n). It is guaranteed that neighboring heights are not equal to each other (that is, ai≠ai+1 for all i from 1 to n−1).

Peaks of mountains on the segment [l,r] (from l to r) are called indexes i such that l<i<r, ai−1ai+1. It is worth noting that the boundary indexes l and r for the segment are not peaks. For example, if n=8 and a=[3,1,4,1,5,9,2,6], then the segment [1,8] has only two peaks (with indexes 3 and 6), and there are no peaks on the segment [3,6].

To break the door, Nastya throws it to a segment [l,l+k−1] of consecutive mountains of length k (1≤l≤n−k+1). When the door touches the peaks of the mountains, it breaks into two parts, after that these parts will continue to fall in different halves and also break into pieces when touching the peaks of the mountains, and so on. Formally, the number of parts that the door will break into will be equal to p+1, where p is the number of peaks on the segment [l,l+k−1].

Nastya wants to break it into as many pieces as possible. Help her choose such a segment of mountains [l,l+k−1] that the number of peaks on it is maximum. If there are several optimal segments, Nastya wants to find one for which the value l is minimal.

Formally, you need to choose a segment of mountains [l,l+k−1] that has the maximum number of peaks. Among all such segments, you need to find the segment that has the minimum possible value l.

Input
The first line contains an integer t (1≤t≤104) — the number of test cases. Then the descriptions of the test cases follow.

The first line of each test case contains two integers n and k (3≤k≤n≤2⋅105) — the number of mountains and the length of the door.

The second line of the input data set contains n integers a1,a2,…,an (0≤ai≤109, ai≠ai+1) — the heights of mountains.

It is guaranteed that the sum of n over all the test cases will not exceed 2⋅105.

Output
For each test case, output two integers t and l — the maximum number of parts that the door can split into, and the left border of the segment of length k that the door should be reset to.

Example

input
5
8 6
1 2 4 1 2 4 1 2
5 3
3 2 3 2 1
10 4
4 3 4 3 2 3 2 1 0 1
15 7
3 7 4 8 2 3 4 5 21 2 3 4 2 1 3
7 5
1 2 3 4 5 6 1
output
3 2
2 2
2 1
3 1
2 3

  题目很长(cf的好些题目都很长),我也意识到我读题很慢,甚至长一些的题目读起来不知不觉二三十分钟就过去了,这个只能慢慢练了。这个题目大意是:有n座山,其中有几个极大值(peak)山峰,每遇到一个山峰就可以将一块门板一分为二(就是拥有的碎片数+1),求在走过一定数量座山(一定区间内)情况下能够得到最多碎片数,输出碎片数,并输出选取走过山的第一座位置(此区间的左端点),如果有多种情况得到的碎片数最多,输出最小的位置(端点)。
  看到这个题,我的思路就是枚举所有的山,每当遇到一个山峰时存储这个山峰的下标并向前枚举检索在规定区间内能够得到的最大碎片数目并更新碎片数和区间端点值。因为只有遇到更多的碎片数时才会更新碎片数和端点值,所以最终得到的端点值一定是最小的(较大的情况不会去更新)。这个想法很简单,所以wa,代码:

#include<iostream>
#include<iomanip>
#include<algorithm>
#include<cmath>
#include<string>
#include<cstring>
#include<stack>
typedef long long ll;
using namespace std;
int a[200010],p[200010];
int main()
{
    cin.tie(0);
    ios::sync_with_stdio(0);
    int t,n,s,peaks,ans,aans,l;
    cin>>t;
    for(int o=1;o<=t;o++)
    {
        cin>>n>>s;
        ans=0;
        for(int i=1,j=1;i<=n;i++)
        {
            cin>>a[i];
            if(i>2&&a[i]<a[i-1]&&a[i-2]<a[i-1])
            {
                p[j]=i-1;
                aans=1;
                for(int k=j;k>=1;k--)
                {
                    if(p[k]<i-s+2) break;
                    else aans++;
                }
                if(ans<aans)
                {
                    ans=aans;
                    if(i<=s-1) l=1;
                    else l=i-s+1;  }
                j++;
            }
            if(i==n&&j==1) { ans=1; l=1; }
        }
        cout<<ans<<" "<<l<<endl;
    }
}


  错误的原因是超时,其实在此之前也错了两次,两次选错语言和数组越界就不用说了,但这次改起来就麻烦了。而我想到的解决办法不是每个山峰向前枚举了,而是用数组记录所有的山峰同时再单独记录一个山峰,每当得到一个新的山峰时就判断一下记录的山峰能不能和当前山峰在同一个区间内(注意,一个区间山峰不能在端点处,所以下标的处理上会有些麻烦),如果不在就向后面更新,这样就不用再枚举了。ac代码:

#include<iostream>
#include<iomanip>
#include<algorithm>
#include<cmath>
#include<string>
#include<cstring>
#include<stack>
typedef long long ll;
using namespace std;
int a[200010],p[200010];
int main()
{
    cin.tie(0);
    ios::sync_with_stdio(0);
    int t,n,s,ans,aans,l,tp;
    cin>>t;
    for(int o=1;o<=t;o++)
    {
        cin>>n>>s;
        ans=0; tp=1;
        for(int i=1,j=1;i<=n;++i)
        {
            cin>>a[i];
            if(i>2&&a[i]<a[i-1]&&a[i-2]<a[i-1])
            {
                p[j]=i-1;
                if(p[j]-p[tp]>s-3) tp++;
                aans=j-tp+2;
                if(ans<aans)
                {
                    ans=aans;
                    if(i<=s-1) l=1;
                    else l=i-s+1;  }
                ++j;
            }
            if(i==n&&j==1) { ans=1; l=1; }
        }
        cout<<ans<<" "<<l<<endl;
    }
}


  官方的题解我看过了,说实话有点半懂不懂,英文的嘛,不过肯定和我的不一样,而且好像也简单一些。不过我总感觉这种优化时间复杂度的方法虽然不是从老师那学来的但却是从日常学习中得来的,而虽然做题的方法我也没想过用什么算法但其间都有平时算法的影子。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值