TGU算法课程贪心与动态规划

本文介绍了五个C++编程题目,涉及动态规划和贪心策略,包括货物搬运的最少操作次数、村庄间最短花费时间、背包问题、上升子序列计算及有依赖背包问题。通过分析问题特点,运用动态规划的转移方程和贪心算法的最优子结构,给出了解决这些问题的思路和代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Problem A 货物搬运

题目描述

码头上有一批货物,它们被摆放成了n摞,每摞的个数(高度)为di,并排成一排。

现在包工头酋长想用一台特殊的搬运机,用最少的次数,将这批货物全部取走。

这台搬运机的工作原理是:一次可以取走相邻几排最下面一层的所有货物。

例如:当货物共有4摞,每摞高度分别为 2 3 1 2 时,我们可以采用如下策略使得总使用次数最少

第一步:由于四摞货物是相邻的,取走这四摞货物最下面的一排货物,当前变为 1 2 0 1

第二步:此时第三摞货物取空,第一、二摞和第四摞不相邻,取走前两摞货最下面一排,当前变为 0 1 0 1

第三步、第四步:分别取走第二摞和第四摞货物最下面一排,从而将这批货物全部取走

因此使用这台搬运机的最少次数为:4

I/O格式样例

输入格式

输入数据包含两行

第一行包括一个整数n, 含义如题面所述 ( 1 <= n <= 100000)

第二行包括n个整数di, 表示每摞货物的高度(个数) (0 <= di <= 10000)

输出格式

输出包含一个整数, 表示使用搬运机的最少次数

输入样例

4
2 3 1 2

输出样例

4

思路分析

我们通过分析这道题目,可以获得的信息是,我们需要对数组进行若干次操作,每一次操作可将1个或相邻的多个数-1,最终将其全部置为0。

那么可以知道,只要有数连续就将其并在一起处理这样的子问题最优可以导致全局最优。

证明:∀i∈(1,n-2)满足 a[i]>0&&a[i-1]>0&&a[i+1]>0 对a[i-1]a[i],a[i+1]进行操作,次数为2次,对a[i-1],a[i]a[i+1]操作次数为2次,对a[i-1]a[i]a[i+1]操作次数为1次,对于3者中最小的值,会优先被移走导致了连续数的断裂。

设a[i-1]>a[i] ,a[i+1]>a[i]我们将3个数先置为a[i-1]=a[i]=a[i+1]时,需要进行a[i-1]-a[i]+a[i+1]-a[i]次操作,总操作次数为(a[i-1]-a[i])+(a[i+1]-a[i])+a[i]=a[i-1]+a[i+1]-a[i];优先将3个数放在一起处理,总操作次数为a[i]+(a[i-1]-a[i])+(a[i+1]-a[i])=a[i-1]+a[i+1]-a[i],结果相等;

设a[i-1]<a[i]<a[i+1]优先全置为a[i-1]的操作次数为(a[i]-a[i-1])+(a[i+1]-a[i-1]),总操作次数为a[i+1]+a[i]-a[i-1];将3个数放在一起处理,操作次数为a[i]-a[i-1]+a[i+1]-a[i-1]+a[i-1]=a[i+1]+a[i]-a[i-1];

设a[i-1]>a[i]>a[i+1],优先全置为a[i+1]的操作次数为(a[i-1]-a[i+1])+(a[i]-a[i+1]),总操作次数为a[i-1]+a[i]-a[i+1];将3个数放在一起处理,操作次数为a[i-1]-a[i+1]+a[i]-a[i+1]+a[i+1]=a[i-1]+a[i]-a[i+1];

所以,将连续的数分开处理和合并处理结果相同。由此,我们可以发现,对于当前的高度a[i]所需要的搬运次数只和是a[i]与a[i-1]大小有关。当前状态只和上一状态有关且问题无重叠性,用动态规划求解。

我们设dp[i]为以第i个货物结尾时,所需要的最少操作次数,通过以上的证明我们可以发现,dp[i]是一个不下降的序列。

当a[i]<=a[i-1]时,对于a[i-1]需要的最少操作为j,一定可以j次操作内将a[i]置为0,所以dp[i]=dp[i-1]
当a[i]>a[i-1]时,前一次的最小操作会导致a[i-1]先置为0,a[i]还有剩余,数组会发生断裂,所以对于当前的a[i],最小操作次数dp[i]=dp[i-1]+(a[i]-a[i-1])
我们可以写出转移方程
d p [ i ] = { d p [ i − 1 ] a[i]<=a[i-1] d p [ i − 1 ] + a [ i ] − a [ i − 1 ] a[i]>a[i-1] dp[i]=\begin{cases} dp[i-1] & & \text{a[i]<=a[i-1]} \\ dp[i-1]+a[i]-a[i-1] & & \text{a[i]>a[i-1]} \\ \end{cases} dp[i]={dp[i1]dp[i1]+a[i]a[i1]a[i]<=a[i-1]a[i]>a[i-1]
换言之 d p [ i ] = d p [ i − 1 ] + m a x ( 0 , a [ i ] − a [ i − 1 ] ) dp[i]=dp[i-1]+max(0,a[i]-a[i-1]) dp[i]=dp[i1]+max(0,a[i]a[i1])
动态规划的边界为dp[0]=a[0],我们要求的答案为dp[n-1]
我们发现,dp[i]只与dp[i-1]有关,所以我们可以拿一个动态变量保存dp数组的值以优化时空效率

代码块

#include<bits/stdc++.h>
using namespace std;
int main(){
    int ans=0,n;
    cin>>n;
    vector<int> a(n);//vector动态数组,没学过的小伙伴可以直接用int a[n],后文不做解释
    for (int i=0;i<n;i++) 
    	cin>>a[i];
    int dp=a[0];//定义DP边界,相当于dp[0]=a[0]
    for (int i=1;i<n;i++)
        dp+=max(0,a[i]-a[i-1]);//转移方程
    cout<<dp<<endl;
    return 0;
}


Problem B 拉不拉部落

题目描述

牛牛是货拉拉拉不拉拉布拉多部落的子民,货拉拉拉不拉拉布拉多部落共有n个村庄,被编号为1~n。由于货拉拉拉不拉拉布拉多部落比较落后,并不是每个村庄之间都有通路。货拉拉拉不拉拉布拉多部落的村庄间通路的规则是:每个村庄只会和它相邻的m个村庄之间有通路,且经过该通路的花费时间为两个村庄编号的最大公约数。

现在牛牛在1号村庄,想去n号村庄找酋长,已知每个村庄与它相邻的m个村庄之间有通路,按照如上规则,牛牛最少需要走多久才能到达n号村庄?

I/O格式样例

输入格式

输入数据只有一行,包括两个整数n和m (10 <= n <= 1000)(0 < m < min(n, 100))

输出格式

输出一个数,表示牛牛从1号村庄到达n号村庄所花费的最短时间

输入样例

10 5

输出样例

3

思路分析

这道题目,如果使用贪心算法,我们不能保证,当前的gcd最小就是全局gcd最小,不满足局部最优为全局最优的条件,所以使用动态规划算法。

设出DP数组dp[j]表示,从第i个村庄出发到达第j个村庄时,获得的最短花费时间
显然,本题所求答案为dp[n]

从村庄i到村庄j所需要花费的时间为gcd(i,j)所以转移方程为:
d p [ j ] = m i n ( d p [ j ] , g c d ( i , j ) + d p [ i ] ) dp[j]=min(dp[j],gcd(i,j)+dp[i]) dp[j]=min(dp[j],gcd(i,j)+dp[i])
我们可以使用C++<algorithm>库中内置的__gcd函数来进行求最大公因数的操作
所以我们可以枚举 i 村庄为起点,终点范围为i+1~min(n,i+m),n为目标村庄,i+m为最远移动村庄
对于>n的情况,我们不必考虑,因为我们在已经能到达村庄n时,对于任意大于n的花费时间最少为dp[n]+1。

dp边界为dp[1]=0,因为我们一开始就站在1号村庄,从1到1的跳跃花费为0,对于其他的跳跃情况,边界为dp[i]=∞,但是本题数据比较小,500000即可

代码块

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n,m;
    cin>>n>>m;
    vector<int> dp(n+1,500000);
    dp[1]=0;
    for (int i=1;i<=n;i++) {
        for (int j=min(n,i+m);j>i;j--) {
            dp[j]=min(dp[j],__gcd(i,j)+dp[i]);
        }
    }
    cout<<dp[n]<<endl;
    return 0;
}


Problem C 荣荣的背包

荣荣一个容量是v的背包,现在桌子上有n件物品,每件物品只能使用一次。

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

求解将哪些物品装入背包,可以使得总价值最大。

由于荣荣学姐是一个强迫症,所以她一定要装满这个背包,请你帮她算一算装满背包的最大价值。

题目描述

I/O格式样例

输入格式

输入数据第一行包括两个整数n和v,表示物品数量和背包容量 (0 < n, v <= 1000)
接下来有n行,每行两个整数vi,wi,分别表示第i件物品的体积和价值 (0 < vi, wi <= 1000)

输出格式

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

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

8

思路分析

经典0-1背包问题,转移方程为:
d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] ] + v [ i ] ) dp[j]=max(dp[j],dp[j-w[i]]+v[i]) dp[j]=max(dp[j],dp[jw[i]]+v[i])
w是物品重量weight,v是物品价值value
与01背包不同的是,本题需要恰好装满的情况,对于恰好装满,我们可以先预设背包里,每一个值都为-INF。dp[j]表示对于大小为j的背包可以获得的最大价值,当dp[j]=-INF时,说明重量为j时无法恰好装满,所以转移状态需要增加一步判断。
伪代码

if (dp[j]<0) dp[j]=-INF;

对于我们要获取的答案为dp[V],如果V是负无穷,说明重量为V无法正好装满,输出0,否则,输出dp[V]。

代码块

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n,V,ans=-1,i,j;
    cin>>n>>V;
    vector<int> dp(V+1,INT_MIN),w(n),v(n);
    for (int i=0;i<n;i++) {
        cin>>w[i]>>v[i];
    }
    dp[0]=0;
    for (i=0;i<n;i++) {
        for (j=V;j>=w[i];j--) {
        	dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
          	if (dp[j]<0) dp[j]=INT_MIN;
        }
    }
    cout<<(dp[V]<0? 0:dp[V])<<endl;
    return 0;
}


Problem D 时北北的上升子序列

题目描述

给定一个长度为n的序列,从序列中按顺序任取元素得到的严格单调递增的序列被称作上升子序列。

由于时北北是一个完美主义者,所以她想用若干上升子序列覆盖住整个区间,请你帮她算一算整个区间最少可以用多少个上升子序列完全覆盖住。(只有一个元素也算作一个上升子序列)

I/O格式样例

输入格式

输入数据包括两行。
第一行包括一个整数n,表示序列长度。(1 <= n <= 1000)
第二行包含n个整数ai,表示这个序列每一位上的元素。(-10^9 <= ai <= 10^9)

输出格式

输出一个整数,表示最少用多少个互不相交的上升子序列可以完全覆盖住整个区间。

输入样例

7
3 1 2 1 8 5 6

输出样例

3

思路分析

我们需要知道本题的前置LIS(Longest Increasing Subsequence)最长上升子序列问题
对于基础的LIS最长上升子序列问题,转移方程为:
d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) dp[i]=max(dp[i],dp[j]+1) dp[i]=max(dp[i],dp[j]+1)
对于本题而言,要求一个序列中可以被分割成多少个LIS,只需要求有多少个LIS起点就行,换言之,对于每一个LIS,都是互相独立的,所以我们不难想到用贪心求解。

对于当前遍历到的数a[i],如果a[i]>其中一个LIS起点,就可以将a[i]作为这个LIS的新起点,当a[i]不满足成为任何一个已经创建的LCS新起点时,a[i]本身可以作为一个新起点,最终返回起点个数,所以本题可以采用队列或动态数组的方法来做,输出动态数组的长度。

代码块

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n;
    cin>>n;
    vector<int> nums(n);
    vector<int> ans;//保存LIS起点
    for (int i=0;i<n;i++) cin>>nums[i];
    ans.push_back(nums[0]);
    for (int i=1;i<n;i++) {
        bool flag=1;//记录当前的nums[i]是否被更新
        for (int j=0;j<ans.size();j++) {
            if (nums[i]>ans[j]){//如果可以作为新的起点
                ans[j]=nums[i];//替换第j个LCS的起点
                flag=0;//nums[i]发生过替换
                break;
            }
        }
        if (flag) ans.push_back(nums[i]);//如果nums[i]未发生过替换,以nums[i]为新起点
    }
    cout<<ans.size()<<endl;
    return 0;
}

Problem E 大恺的购物之旅

题目描述

大恺每天呆在寝室里钻研算法知识,终于到了五一假期,他可以快乐地出门购物了。

在购物前,他有一个总的花费预算N,表示他总共花费不能超过N。

同时他有一个想要购买的商品清单,M表示他清单中的商品个数(不包括附属商品)。

来到超市后,他发现他想买的每一个商品都有0~2个附属商品,如果想要购买这些附属商品,一定要先购买所属的原商品。(例如——若商品“篮球”包含两个附属商品:“打气筒”和“篮球袋”,那么大恺可以选择只购买一个篮球;购买篮球加打气筒;购买篮球加篮球袋;或者是把篮球、打气筒和篮球袋全买下来;当然,也可以不买篮球及其附属商品。但是,不可以只买打气筒或篮球袋)

在逛了一圈后,大恺对每一个商品(包括附属商品)都有一个满意度,现在请你帮他算一算,如何在不超过预算的情况下,购买到的商品满意度总和最大。

I/O格式样例

输入格式

输入数据包括M + 1行
第一行包括两个整数N,M,分别表示大恺的总花费预算和愿望清单中商品的数量(不包括附属商品)
接下来的M行,每行的前三个数v, p, q分别表示当前商品的价钱,满意度以及该商品的附属商品数量
每行接下来包括2 * q个数,依次表示当前商品附属商品的价钱和满意度

输出格式

输出一个正整数,表示不超过预算限制下,大恺可以获得的最大满意度

输入样例

1000 3
800 5 0
300 1 1 200 2
400 2 2 100 1 200 1

输出样例

6

思路分析

本题是一道有依赖背包问题,什么叫做有依赖背包?顾名思义,其中某些物品的购买需要依赖部分物品的购买,只有购买了先置的物品,才可以购买附属物品,对于这样的背包问题,可以将其转化为01背包问题和分组背包问题求解。

对本题而言,我们有主件w[i][0]和附件w[i][q](q={1,2})购买附件的前提是购买主件,我们可以将问题拆分成4个子问题来看:

  1. 只考虑主件
  2. 考虑主件+附件1
  3. 考虑主件+附件2
  4. 考虑主件+附件1+附件2

转移方程为:
d p [ j ] = { m a x ( d p [ j ] , d p [ j − w [ i ] [ 0 ] ] + v [ i ] [ 0 ] )  主件 m a x ( d p [ j ] , d p [ j − w [ i ] [ 0 ] − w [ i ] [ 1 ] ] + v [ i ] [ 0 ] + v [ i ] [ 1 ] )  主+附1 m a x ( d p [ j ] , d p [ j − w [ i ] [ 0 ] − w [ i ] [ 2 ] ] + v [ i ] [ 0 ] + v [ i ] [ 2 ] )  主+附2 m a x ( d p [ j ] , d p [ j − w [ i ] [ 0 ] − w [ i ] [ 1 ] − w [ i ] [ 2 ] ] + v [ i ] [ 0 ] + v [ i ] [ 1 ] + v [ i ] [ 2 ] )  主+附1+附2 dp[j]= \begin{cases} max(dp[j],dp[j-w[i][0]]+v[i][0])\text{ 主件}\\ max(dp[j],dp[j-w[i][0]-w[i][1]]+v[i][0]+v[i][1])\text{ 主+附1}\\ max(dp[j],dp[j-w[i][0]-w[i][2]]+v[i][0]+v[i][2])\text{ 主+附2}\\ max(dp[j],dp[j-w[i][0]-w[i][1]-w[i][2]]+v[i][0]+v[i][1]+v[i][2])\text{ 主+附1+附2}\\ \end{cases} dp[j]=max(dp[j],dp[jw[i][0]]+v[i][0]) 主件max(dp[j],dp[jw[i][0]w[i][1]]+v[i][0]+v[i][1]) +1max(dp[j],dp[jw[i][0]w[i][2]]+v[i][0]+v[i][2]) +2max(dp[j],dp[jw[i][0]w[i][1]w[i][2]]+v[i][0]+v[i][1]+v[i][2]) +1+2
在第i组中,第q个附件的重量为w[i][q],第q个附件的价值为v[i][q]

代码块

#include<bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(false);
    int n,V,q;
    cin>>V>>n;
    vector<int> dp(V+1);
    vector<vector<int>> w(n,vector<int>(3)),v(n,vector<int>(3));
    for (int i=0;i<n;i++){
        cin>>w[i][0]>>v[i][0]>>q;
        for (int j=1;j<=q;j++) {//读入附件
            cin>>w[i][j]>>v[i][j];
        }
    }
    for (int i=0;i<n;i++) {//遍历所有的组
        for (int j=V;j>=w[i][0];j--) {//每个物品只允许取一次,所以要倒序遍历重量
        	//遍历到w[i][0]是保证了主件能取
			dp[j] = max(dp[j],dp[j-w[i][0]]+v[i][0]);//取主件的转移方程
			if (j-w[i][0]-w[i][1]>=0)
				dp[j] = max(dp[j],dp[j-w[i][0]-w[i][1]]+v[i][0]+v[i][1]);//主+附1
			if (j-w[i][0]-w[i][2]>=0)
				dp[j] = max(dp[j],dp[j-w[i][0]-w[i][2]]+v[i][0]+v[i][2]);//主+附2
			if (j-w[i][0]-w[i][1]-w[i][2]>=0)
				dp[j] = max(dp[j],dp[j-w[i][0]-w[i][1]-w[i][2]]+v[i][0]+v[i][1]+v[i][2]);//主+附1+附2
        }
    }
    cout<<dp[V]<<endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Capzera

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值