【蓝桥杯】每日练习 Day21

目录

前言

背包

货币问题

分析

代码

砝码称重

分析

代码

区间dp

石子合并

分析

代码

游戏

分析

代码


前言

今天依旧不讲数论 ,明天我会把数论剩下的组合数求法容斥原理 补上,今天带来的是背包 和**区间dp**的超级简单题总共四道。


背包

先来讲一下什么是背包问题 ,背包问题又被称为有限制的选法问题 ,注意这里的选法是没有顺序 要求的。按照这个思路,如果我们发现一道dp问题没有对顺序的要求 ,那么就可以思考 一下问题是不是某种背包模型了。

但是没有顺序的要求并不是 说我们可以随便选,随便选的话很容易 就会出现某个元素选的个数超过要求 的情况,为了避免 这种错误呢,背包问题一般都用i个物品的所有选法 作为划分 ,每次只在原来的基础上增加一种物品 的选法,这样就可以很好的控制每种物品选择的数量,巧妙的避开了乱选的情况。

而又因为每种物品可能不止一个 ,所以我们又需要一维来枚举 这个物品选择的个数 ,那么状态表示就变为了**dp[i][j]** ,一般情况下背包问题 都是二维 的,少数情况可以 优化到一维 ------参考**01背包** 和完全背包 这两种。(不会背包模板的同学请先移步至背包九讲专题_哔哩哔哩_bilibili

讲完了背包问题的大致思路以后我们就来写两道题练练手。


货币问题


分析

可以发现题目与选择的顺序无关 ,只与选择的方案有关 ,所以我们就可以尝试能否 通过背包 来求解,发现对于每种货币都没有选择限制,所以考虑完全背包

那么问题就转化成了完全背包求方案数 ,直接套模板就好。(注意这里的方案数空集要初始化为1 ------求方案数的dp问题 一般都需要初始化空集,大家记住就好)


代码

cpp 复制代码
//完全背包
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 10010;
int n, v;
LL dp[N]; 
int nums[N];
int main()
{
    scanf("%d%d", &n, &v);
    for(int i = 0; i < n; i++)
        scanf("%d", nums + i);
    dp[0] = 1;
    for(int i = 0; i < n; i++)
        for(int j = nums[i]; j <= v; j++)
            dp[j] += dp[j - nums[i]];
    printf("%lld", dp[v]);
    return 0;
}

砝码称重


分析

发现每种砝码可以分为选或不选 两种情况,而且本题与选择的顺序无关 只与选择的方案有关 所以考虑**01背包** 。(也可以说是状态机dp

每种砝码选择的话需要分为两种情况 ------放左边或放右边

显然因为左右只是相对的 ,所以我们的所有方案也可以分为两种情况 (分别是正数域和负数域 ),对于这道题我们只考虑正数域

不过这里有一个小细节 ,就是当枚举的重量小于当前砝码的重量时可能会变成负数相对的 一种选法在负数域时遇到这种情况也有可能会变成正数

也可以这样考虑------所有结果为正数的情况只会由两种情况变化而来 ,即:正数 变成正数负数 变成正数

因为我们要考虑所有的正数 的情况所以这种方案也要记录,具体操作就是取abs


代码

cpp 复制代码
/*
    每个砝码都三种状态:放左边,右边和不放,显然不放不会改变方案的总数,所以不考虑
    总量不会超过1e5,所以打表
*/
#include<iostream>
using namespace std;
const int N = 110, M = 1e5 + 10;
int n;
bool dp[N][M];
int nums[N];
​
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d", nums + i);
    dp[0][0] = true;
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= 1e5 - nums[i]; j++)
            if(dp[i - 1][j]) //只用上一层的可以
            {
                dp[i][j] = true; 
                dp[i][abs(j - nums[i])] = true;
                dp[i][j + nums[i]] = true;
            }
    int l = 0;
    for(int i = 1; i < M; i++)
        l += dp[n][i];
    printf("%d", l);
    
    return 0;
}

区间dp

区间dp 呢,我也没有做多少题,不是很了解。我能发现的区间dp大概可以理解为状态是区间状态的子状态依旧是区间 ,所以对于区间dp问题我们就可以通过求子区间来推导出原区间。


石子合并


分析

区间dp的一道模板题 ,状态表示是**dp[l][r]** ,两维分别代表区间的左端点和右端点

状态转移:dp[l][r] = min(dp[l][k] + dp[k + 1][r]) + s[r] - s[l - 1]

利用分治 的思想将区间划分为两段 ,这样就可以利用子问题的结果求值了(s为前缀和数组)恍惚间觉得这道题好像讲过(

这道题注意一下初始化即可,时间复杂度为O(n^3)


代码

cpp 复制代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 310;
int n;
int nums[N];
int s[N];
int dp[N][N];
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d", nums + i);
    memset(dp, 0x3f, sizeof dp);
    for(int i = 1; i <= n; i++)
    {
        s[i] = s[i - 1] + nums[i];
        dp[i][i] = 0;
    }
    
    for(int i = 2; i <= n; i++) //枚举区间长度
    {
        for(int j = 1; j + i - 1 <= n; j++) //枚举左端点
        {
            int l = j, r = j + i - 1;
            for(int k = l; k < r; k++)
                dp[l][r] = min(dp[l][k] + dp[k + 1][r], dp[l][r]);
            dp[l][r] += s[r] - s[l - 1];
        }
    }
    printf("%d", dp[1][n]);
    return 0;
}

游戏


分析

这游戏一点也不好玩QAQ。

说是区间dp但我认为这道题主要是**思维 + 博弈论**。

首先我们发现无论 游戏如何进行AB总分 都是不变 的,而胜利的条件是一方大于另一方 ,可以写为**A > B**。

这里如果我们去枚举的话可以发现**AB** 是两种状态 ,这里有一个常见的小思路 ,就是移项大小关系转化成差值A - B > 0 。(主播依稀记得前面有一道前缀和 + 贪心的题目也是使用这种思路优化。这就不得不提复盘的好处了,不然主播学一点忘一点。)这样优化以后就只需要 考虑一个变量了。

之后就是我们博弈论 的思考方式了,即------最差情况下的最好

什么意思呢?其实就是双方都是绝顶聪明 的,而**01游戏的特点就是一方若是最好** ,那么另一方必然是最差,所以说这个最差是对方造成的,我们需要在这种情况下找出最优方案。

再加上我们前面的思路,我们选择求**A - B的最大值** ,而对于A - B ,可以划分为两个集合 ,一种是选择左端的最佳情况,可以表示为:

a[l] - (B - A) 注意这里的A 和 B只是表示的相对状态并不是数值!

同理,另一端可以表示为**a[r] - (B - A)** ,当前情况最优就是两者的最大值

而求出了**A - B = d** 如何求出AB呢?

前面我们说了01游戏的特点是**A + B = s** ,这是一个恒等式 ,所以接下来我们对两者联立,就可以求出:

A = (s + d) / 2

B = (s - d) / 2

接下来是代码。


代码

cpp 复制代码
#include<iostream>
using namespace std;
const int N = 110;
int a[N];
int n, s;
int dp[N][N];
int dfs(int l, int r)
{
    if(l == r) return a[l];
    if(dp[l][r]) return dp[l][r];
    int x = a[l] - dfs(l + 1, r);
    x = max(a[r] - dfs(l, r - 1), x);
    return dp[l][r] = x;
}
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", a + i);
        s += a[i];
    }
​
    int l = dfs(1, n); // 计算 A - B
    printf("%d %d", (s + l) / 2, (s - l) / 2); //01游戏
    return 0;
}
相关推荐
@蓝莓果粒茶几秒前
LeetCode第131题_分割回文串
开发语言·前端·算法·leetcode·职场和发展·c#·.net
雾里看山21 分钟前
算法思想之滑动窗口(一)
算法·leetcode·推荐算法
共享家952732 分钟前
深入探究C++ 运算符重载:以日期类为例
c++
老歌老听老掉牙1 小时前
C++使用Qt Charts创建数据可视化图表
c++·qt·信息可视化
猫咪-95271 小时前
链表算法中常用操作和技巧
数据结构·算法·链表
被AI抢饭碗的人1 小时前
算法题(114):矩阵距离
算法
javaisC1 小时前
数据结构----------顺序查找,折半查找和分块查找(java实现)
java·数据结构·算法
Mcband1 小时前
主流程发起,去除子流程的时长计算问题
java·前端·算法
爽帅_2 小时前
【C++】STL库_stack_queue 的模拟实现
开发语言·c++
。。。9042 小时前
C++中,应尽可能将引用形参声明为const
开发语言·c++