算法模板7:动态规划+贪心


萨尔镇楼
在这里插入图片描述

五. 动态规划

5.1 背包问题

dp[j] 表示在容量为 j 的背包中,能够获得的最大价值。

01背包模板

每个物品只能选一次。

for (int i = 1; i <= n; i++) {        // 遍历物品
    for (int j = m; j >= w[i]; j--) { // 遍历容量,从大到小
        dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
    }
}
完全背包模板

每个物品可以选无限次。

for (int i = 1; i <= n; i++) {       // 遍历物品
    for (int j = w[i]; j <= m; j++) { // 遍历容量,从小到大
        dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
    }
}
多重背包模板

每个物品有固定数量。

for (int i = 1; i <= n; i++) {
    for (int k = 1; k <= s[i]; k++) { // 枚举每个物品的数量
        for (int j = m; j >= w[i]; j--) {
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
}

5.2 线性 DP

最大子数组和

在一个整数数组中找到一个具有最大和的连续子数组。

dp[i] 表示以第 i 个元素结尾的最大子数组和。

int dp[n]; 
dp[0] = a[0];
int res = dp[0];
for (int i = 1; i < n; i++) {
    dp[i] = max(a[i], dp[i - 1] + a[i]);
    res = max(res, dp[i]);
}
最长递增子序列: LIS模板

dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。

/*
    状态转移dp[i] = max{ 1.dp[j] + 1 };  j<i; a[j]<a[i];
    d[i]是以i结尾的最长上升子序列
    与i之前的 每个a[j]<a[i]的 j的位置的最长上升子序列+1后的值比较
*/
void solve(){   // 参考挑战程序设计入门经典;
    for(int i = 0; i < n; ++i){  
        dp[i] = 1;  
        for(int j = 0; j < i; ++j){  
            if(a[j] < a[i]){  
                dp[i] = max(dp[i], dp[j] + 1);  
            } } }
}  
/* 
    优化方法:
    dp[i]表示长度为i+1的上升子序列的最末尾元素  
    找到第一个比dp末尾大的来代替 
*/

    void solve() {  
        for (int i = 0; i < n; ++i){
            dp[i] = INF;
        }
        for (int i = 0; i < n; ++i) {  
            *lower_bound(dp, dp + n, a[i]) = a[i];  //  返回一个指针  
        }  
        printf("%d\n", *lower_bound(dp, dp + n, INF) - dp;  
    }
/*  
    函数lower_bound()返回一个 iterator 它指向在[first,last)标记的有序序列中可以插入value,而不会破坏容器顺序的第一个位置,而这个位置标记了一个不小于value的值。
*/
最长公共子序列 LCS
  • dp[i][j] 表示字符串 s1 的前 i 个字符和字符串 s2 的前 j 个字符的最长公共子序列的长度。
void solve() {  
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {  
            if (s1[i] == s2[j]) {  
                dp[i + 1][j + 1] = dp[i][j] + 1;  
            }else {  
                dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]);  
            } } }
}  

5.3 区间 DP

  • 求解需要分割或合并区间的最优值。
  1. 戳气球:LeetCode 312。
  2. 石子合并问题:AcWing 282。
for (int len = 2; len <= n; len++) {      // 枚举区间长度
    for (int l = 1; l + len - 1 <= n; l++) { // 枚举左端点
        int r = l + len - 1;              // 计算右端点
        for (int k = l; k < r; k++) {     // 枚举分割点
            dp[l][r] = max(dp[l][r], dp[l][k] + dp[k + 1][r] + cost);
        } }
}

5.4 状态压缩 DP

  • 适用于子集问题,利用位运算压缩状态。
  • 状态表示为某个集合的子集。
  1. 旅行商问题(TSP):AcWing 91。
  2. 划分子集和问题:LeetCode 698。
for (int mask = 0; mask < (1 << n); mask++) {  // 枚举所有状态
    for (int i = 0; i < n; i++) {             // 枚举当前状态下可选元素
        if (mask & (1 << i)) {
            dp[mask] = min(dp[mask], dp[mask ^ (1 << i)] + cost);
        }
    }
}

5.5 树形 DP

  • 用于树形结构的最优值问题。
  1. 树的直径:LeetCode 124。
  2. 树的最大路径和:AcWing 285。
void dfs(int u, int parent) {
    for (int v : graph[u]) {
        if (v == parent) continue;
        dfs(v, u);
        dp[u] = max(dp[u], dp[v] + value[v]);
    }
}

5.6 记忆化搜索

  • 使用递归 + 记忆化数组避免重复计算。
  • 自底向上递归。
  1. 爬楼梯问题:LeetCode 70。
  2. 三角形最小路径和:LeetCode 120。
int dfs(int u) {
    if (vis[u]) return dp[u];
    vis[u] = 1;
    dp[u] = ...; // 状态转移
    return dp[u];
}

六. 贪心

6.1 区间选点

给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。

思路:

  1. 将所有区间按照右端点排序
  2. 遍历所有区间,ed 初始化为无穷小
    如果本次区间不能覆盖上次区间的右端点,ed<e[i].l,那么需要选择一个新的点 res++;ed=e[i].r
    如果本次区间可以覆盖上次区间的右端点,则进行下一轮循环
#include<bits/stdc++.h>
using namespace std;
int n;
struct SS{
    int l,r;
}e[100010];
bool cmp(SS x,SS y)
{
    return x.r<y.r;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        e[i]={x,y};
    }
    sort(e+1,e+1+n,cmp);
    int ed=-2e8,res=0;
    for(int i=1;i<=n;i++)
    {
        if(e[i].l>ed)
        {
            res++;
            ed=e[i].r;
        }
    }
    cout<<res;
    return 0;
}

6.2 区间分组

给定 N 个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
输出最小组数。

应用场景:
有若干个活动,第 i 个活动开始时间和结束时间是 [SiSi,fifi],同一个教室安排的活动之间不能交叠,求要安排所有活动,少需要几个教室?

思路:

  1. 将所有区间按照左端点从小到大排序
  2. 用小根堆维护每一个不相交区间的右端点的最大值
  3. 若区间之间有交集,那么增加一个新的教室
#include<bits/stdc++.h>
using namespace std;
int n;
struct SS{
    int l,r;
}e[100010];
bool cmp(SS x,SS y)
{
    return x.l<y.l;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        e[i]={x,y};
    }
    sort(e+1,e+1+n,cmp);
    //用小根堆来维护所有组右端点的最大值
    //堆中每一个值存的是每个组的右端点的最大值 
    priority_queue<int,vector<int>,greater<int>>heap;
    for(int i=1;i<=n;i++)
    {
        auto t=e[i];
        //若堆为空或者堆顶元素 >= 现在区间左端点,说明有交集,不能合并 
        if(heap.empty()||heap.top()>=t.l) heap.push(t.r);
        else
        {
            //更新当前区间的右端点 
            heap.pop(); 
            heap.push(t.r);
        }
    }
    //输出组数 
    cout<<heap.size();
    return 0; 
}

6.3 区间覆盖

给定 N 个闭区间 [ai,bi] 以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。
输出最少区间数,如果无法完全覆盖则输出 -1。

思路:

  1. 根据所有区间的左端点从小到大排序
  2. 从前往后枚举每个区间,在所有能覆盖 st 的区间里,选择右端点最大的区间,然后将 st 更新为右端点的最大值
#include<bits/stdc++.h>
using namespace std;
int st,ed;
int n;
struct SS{
    int l,r;
}e[100010];
bool cmp(SS x,SS y)
{
    return x.l<y.l;
}
int main()
{
    cin>>st>>ed;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        e[i]={x,y};
    }
    sort(e+1,e+1+n,cmp);
    int res=0;
    for(int i=1;i<=n;i++)
    {
        int j=i,r=-2e8; 
        while(j<n&&e[j].l<=st)//寻找右端点最大的左端点能覆盖st的区间
        {
            r=max(r,e[j].r);
            j++;
        }
        if(r<st)//不能覆盖区间 
        {
            cout<<"-1";
            break;
        }
        res++;//寻找一次,次数+1
        st=r;//更新st 
        if(r>=ed)//已经覆盖区间 
        {
            cout<<res;
            break;
        }
        i=j-1;
    }   
    return 0;
}

6.4 最大不相交区间数量

给定 N 个闭区间 [ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。
输出可选取区间的最大数量。

思路:

  1. 根据所有区间的右端点从小到大排序
  2. 从前往后枚举每个区间,如果当前区间已经包含点,则 pass,否则,选择当前区间的右端点
    3.ps: 该题实质上是区间选点的本质
#include<bits/stdc++.h>
using namespace std;
int n;
struct SS{
    int l,r;
}e[100010];
bool cmp(SS x,SS y)
{
    return x.r<y.r;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        e[i]={x,y};
    }
    sort(e+1,e+1+n,cmp);
    int ed=-2e8,res=0;
    for(int i=1;i<=n;i++)
    {
        if(e[i].l>ed)
        {
            res++;
            ed=e[i].r;
        }
    }
    cout<<res;
    return 0;
}

6.5 摆动序列

如果相邻数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。例如 [1,7,4,9,2,5] 是一个摆动序列,差值 (6,-3,5,-7,3) 正负交替出现。
[1,4,7,2,5] 和 [1,7,8,6,4,2,3] 不是摆动序列。
给定一个整数序列,返回作为摆动序列的最长子序列的长度 。通过从原始序列中删除一些(也可以不删除)元素来获得 子序列,剩下的元素保持其原始顺序。
例: 输入:[1,7,4,9,2,5] ,输出 6 输入:[1,7,8,6,4,2,3] ,输出4

思路:

因为摆动序列要求正负交替出现,且数量匹配,所以维护两个变量 updown,分别表示当前元素作为上升或下降趋势时的最长摆动子序列长度。
遍历数组 nums 的每个元素:
如果当前元素 nums[i] 大于前一个元素 nums[i-1],则说明存在上升趋势,更新 up = down + 1
如果 nums[i] 小于前一个元素 nums[i-1],则存在下降趋势,更新 down = up + 1
最终 max(up, down) 即为最长的摆动子序列长度。

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

int maxLength(vector<int> nums) {
    if (nums.size() < 2) return nums.size();
    
    int up = 1, down = 1;  // 初始值设置为1,因为单个元素或相同元素序列也可以看作摆动序列
    for (int i = 1; i < nums.size(); ++i) {
        if (nums[i] > nums[i - 1]) {  // 上升趋势
            up = down + 1;
        } else if (nums[i] < nums[i - 1]) {  // 下降趋势
            down = up + 1;
        }
    }
    return max(up, down);
}

int main() {
	int n;
	cin>>n;
	vector<int>nums(n);
	for(int i=0;i<n;i++){
		cin>>nums[i];
	}
    cout << "最长摆动子序列长度: " << maxLength(nums) << endl;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

梓仁沐白

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

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

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

打赏作者

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

抵扣说明:

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

余额充值