【C++ 算法进阶】算法提升二

news/2024/10/15 7:58:38/

算法提升二

  • 最大分配工资问题 (贪心)
    • 题目
    • 分析
    • 代码详解
  • 数组有序问题 (贪心)
    • 题目
    • 分析
    • 代码详解
  • 消息流问题 (数据结构设计)
    • 题目
    • 分析
    • 代码详解
  • 可乐问题 (Coding能力)
    • 题目
    • 分析
    • 代码详解
  • 司机问题(动态规划)
    • 题目
    • 分析
    • 代码详解

最大分配工资问题 (贪心)

题目

给定数组Hard和Money 长度都为N

Hard[i]表示i号的难度 Money[i]表示i号工作的收入

给定数组ability 长度都为M ability [i] 表示i号人的工作能力

没一号工作都可以提供无数的岗位 难度和收入都一样 但是人的能力必须大于等于这份工作的难度 才可以上班

现在要求返回一个长度为M的数组ans ans[i]表示i号能获得的最好收入

分析

对于这道题目来说 最简单能想到的方法就是遍历整个难度收入数组 就可以找到最高的收入

如果我们想要获得最好的收入我们就需要想到这样的一个贪心点

难度 - 收入 必须要呈正相关 不然一个难度更高收入更低的工作谁会去做呢

有了这个大前提之后我们便可以将难度收入数组排序

  • 同等难度下 我们只要收入最高的
  • 难度越高 收入必须越高

这样子我们就能得到一个数组(当然 为了比较方便 我们也可以使用map来存储)

之后只需要使用二分查找 或者使用map的upper_bond函数就可以找到每号人的最好收入了

代码详解

bool Compare(pair<int, int>& p1, pair<int, int>& p2) {if (p1.first < p2.first){return true ; } else if (p1.first > p2.first) {return false;} else{return p1.second > p2.second;}
}
	sort(vJob.begin(), vJob.end(), Compare);

我们首先写出一个比较函数来job进行排序 (难度小的工作排在难度大的工作前面 同等难度的工作收入高的排在前面)

	// 之后我们创建一个map 开始遍历整个vJob数组 从最低难度开始往后选 我们排除掉难度增加了但是工资没有增加的map<int, int> mapJob = {};mapJob[vJob[0].first] = vJob[0].second;int Pre = vJob[0].first; // pre表示上一份工作的难度for (auto x : vJob){if (Pre != x.first && mapJob[Pre] < x.second){mapJob[x.first] = x.second;Pre = x.first; // pre表示上一份工作的难度}}

之后我们创建一个map有序表 开始遍历整个job数组 从最低难度开始往后 按照同等难度下只要收入最高的 难度增加收入必须增加的原则来插入map

之后我们通过map中的 upper_bound 函数便可以解决该问题

upper_bound(keyi)函数详解

该函数为map的内置函数 使用它可以找到距离keyi最近 并且大于keyi的一个迭代器

	for (auto abliity : vAbility) {auto it = mapJob.upper_bound(abliity);if (it == mapJob.begin()){// 0}else {// 表示是最大值 it -1 即可}}

所以说 我们可以直接使用上面的函数来求出最终答案

数组有序问题 (贪心)

题目

给定一个数组arr 只能对arr中的一个子数组排序 但是想让整个arr抖有序 返回满足这一设定的子数组中 最短的是多长

分析

思路分析

要解决这个问题其实很简单 我们只需要将整个数组排序 然后一一对比其中不一样的值 之后在找到这些值的左右区间即可 这样子做的时间复杂度为N * LogN

比如下图

在这里插入图片描述

当然 这样子并不是最完美的解法 但是应付笔试应该是够用的 因为LogN 肯定是一个很小的数值 在时间复杂度上接近于N

这道题最完美的解法是时间复杂度为N的解法 具体思路如下

在这里插入图片描述

我们首先从左边第一个数字开始 按顺序往右推 如果下一个数字小于我们当前指针指向的数字我们就画上 X 反之则画上 √ 接着指针右移 之后我们找到最后一个打X的位置

在这里插入图片描述

之后我们从右边第一个数字开始 按顺序往左推 如果下一个数字大于我们的当前指针指向的数字 我们就画上X 反之则画上 √ 接着指针左移 之后我们找到最后一个打X的位置

最后的结果差不多是这样 (这里以红色圆圈代表X号)

在这里插入图片描述
我们观察可以发现 这两个位置圈起来的子数组排序下 刚好就是我们需要的答案

原理 (默认顺序)

因为顺序数组一定有前面的数字小于等于后面的数字 后面的数字大于等于前面的数字

所以说对于一个不是顺序结构的数组 我们只需要通过上面的方法找到这两个边界便可以将其变有序了

代码详解

int FindFormLeft(vector<int>& v1) {if (v1.size() == 1){return 0;}int lnKey = 0;for (int i = 0; i < v1.size() - 1; i++)	{if (v1[i] > v1[i + 1]) {lnKey = i + 1;}}return lnKey;
}

从左边开始找的代码如上 接下来只需要写出从右边开始找的代码 之后将他们两个的返回值捕捉即可

消息流问题 (数据结构设计)

题目

已知一个消息流会不断地吐出整数1~N 但不一定按照顺序依次吐出
如果上次打印的序号为i 那么当i+1出现的时候 请打印i+1及其之后接收过的并且连续的所有数
直到1~N全部接收并打印完
请设计出这种数据结构

分析

这其实是一个经典的网络流设计题

我们都知道 网络发动数据肯定不可能全部按顺序到达是吧

比如说我们这里发送一串数据 1 2 3 4

对面接收到的时候可能就是 4 1 3 2

但是我们都知道 数据的顺序是很重要的 所以说我们希望对面接收之后先观察下顺序有没有乱再打印

解法分析

我们要打印的是一个连续的区间 所以说等数据到了之后让其跟我们现在需要的数据对比的方式肯定不可行

那我们想想看 怎么才能让一段数据连续呢? ---- 单链表

我们可以使用单链表来表示一串连续的数据 但是这里也会出现一个问题

我们怎么保证接收到的数据正好让这一串单链表连接起来呢?

这里提供一种思路

我们创建两章哈希表 头表和尾表

头表 存储一串连续的单链表的头部
尾表 存储一串连续的单链表的尾部

图中的数字代表一个节点

图中的每个数字都代表一个节点 后面其实都有一个指针指向空

假如说现在加入一个四号节点

在这里插入图片描述

那么图片就会变成上面这样
在这里插入图片描述

但是由于3 4 5本身就是一串连续的字节流 所以说头表尾表就会变成下面的形式

在这里插入图片描述

头表中只剩下一个3 尾表中只剩下一个5

假设我们现在想要的第一个数字是2 那么当2来到之后只需要和3 头尾相连 我们打印整个单链表就能得到我们想要的结果了 (当然我们要记得处理下头尾表数据)

代码详解

我们首先设计一个信息流节点

struct Node
{int val; 代表节点的编号 我们使用该编号来排序char c; 代表节点传输的信息 Node* next; next指针
};
map<int, Node*> mapHead = {};
map<int, Node*> mapTail = {};
int WantNum = 1; // 我们现在想要的数字

接下来设计下头尾表和我们想要的数字

注意!!!! 在C++中的map和Java中的map不一样 如果我们定义map<int, Node> mapHead 那么穿进去的Node只会是一个拷贝值 他俩的指针没有任何关系 但是在Java中 他们就是一种引用关系
所以说在C++中我们要使用指针

接下来完整代码如下

// 函数:将节点放入链表
void PutNode(Node& n1) {// 首先将节点插入到头表和尾表mapHead[n1.val] = &n1; // 使用指针mapTail[n1.val] = &n1; // 使用指针// 连接前驱节点if (mapTail.count(n1.val - 1) == 1) {mapTail[n1.val - 1]->next = &n1; // 连接前驱mapTail.erase(n1.val - 1); // 删除前驱的记录mapHead.erase(n1.val);}// 连接后继节点if (mapHead.count(n1.val + 1) == 1) {n1.next = mapHead[n1.val + 1]; // 连接后继mapHead.erase(n1.val + 1); // 删除后继的记录mapTail.erase(n1.val);}// 如果当前节点是我们想要的数字if (n1.val == WantNum) {Node* cur = &n1; // 当前节点while (cur) {cout << cur->val << " "; // 打印当前节点的值cur = cur->next; // 移动到下一个节点WantNum++; // 更新想要的数字}cout << endl;// 清除尾表中最后一个节点的记录mapTail.erase(n1.val); // 根据需要清理}
}

大家可以自己生成一些测试用例来验证下代码的正确性

可乐问题 (Coding能力)

题目

购买机只支持硬币支付 且收 退只支持10 50 100三种面额 一次购买只能出一瓶可乐 且投钱和找零都遵循优先使用大钱的原则
需要购买的可乐数目是m
其中手头拥有的10 50 100的数量分别为a b c
可乐的价格为X (x是10的整数倍)
请计算出需要投入硬币次数

分析

这道题目其实就是纯粹考验coding能力 也就是我们如何将一个实际问题转化为工程问题的能力

我们对问题进行分析

首先让我们用大面值的面额的纸币去购买一瓶可乐

这里就出现了三种情况

  1. 比当前面值大的纸币还没有用完 需要用当前纸币补上零钱
  2. 当前纸币足够买x瓶可乐 这里就要考虑找零问题
  3. 当前纸币没有了或者说剩下的额度不足以购买一瓶可乐了 此时我们就要考虑使用下个额度的纸币

转化为代码风格如下
1.

	// 1. 如果前面有剩下的零钱我们要把他用上int lnLastMoney = 0;int lnLastZhang = 0;
// 使用公式进行计算即可
	lnLastMoney += // 剩下的总钱数lnLastZhang += // 剩下的总张数

代码详解

整体代码表示如下

// 定义初始硬币数量
int a = 20;  
int b = 20; 
int c = 1;  
const int ColaMoney = 30;  // 每瓶可乐的价格
int x = 5;  // 需要购买的可乐数量// 找零函数
void Change(vector<int>& vMoney, vector<int>& varr, int lnChange) {for (int i = 0; i < 3; i++) {while (lnChange >= vMoney[i] && varr[i] > 0) {lnChange -= vMoney[i];varr[i]--;  // 减少相应面额的硬币数量}}
}// 购买可乐函数
int BuyCola() {int lnLastMoney = 0;  // 上次剩余的钱int lnLastZhang = 0;  // 上次剩余的张数int lnTotalCoins = 0;  // 总的投币次数// 面额分别是100, 50, 10vector<int> vMoney = { 100, 50, 10 };// 对应的硬币数量vector<int> varr = { c, b, a };while (x > 0) {for (int i = 0; i < 3; i++) {if (x <= 0) break;// 如果有上次剩下的钱if (lnLastZhang != 0 && lnLastMoney != 0) {int lnNeedZhang = ((ColaMoney - lnLastMoney) + vMoney[i] - 1) / vMoney[i];  // 需要的张数,向上取整// 如果硬币数量不够if (lnNeedZhang > varr[i]) {lnLastMoney += varr[i] * vMoney[i];lnLastZhang += varr[i];}else {lnTotalCoins += (lnNeedZhang + lnLastZhang);x--;  // 买掉一瓶可乐varr[i] -= lnNeedZhang;Change(vMoney, varr, lnLastMoney);  // 进行找零lnLastMoney = 0;  // 重置剩余钱数lnLastZhang = 0;  // 重置剩余张数}}// 正常购买可乐的情况int lnBuyCount = (vMoney[i] * varr[i]) / ColaMoney;  // 当前硬币总额能买的可乐数量int lnLastCoinMoney = (vMoney[i] * varr[i]) % ColaMoney;  // 剩余的金额if (lnBuyCount == 0) {// 如果不能买一瓶,则累积剩余钱lnLastMoney += varr[i] * vMoney[i];lnLastZhang += varr[i];}else if (x <= lnBuyCount) {// 如果买够了lnTotalCoins += (x * ColaMoney + vMoney[i] - 1) / vMoney[i];x = 0;}else {// 钱花完了但不够买下全部可乐if (lnLastCoinMoney % vMoney[i] == 0) {lnTotalCoins += (vMoney[i] - (lnLastCoinMoney / vMoney[i]));x -= lnBuyCount;  // 购买的可乐数量lnLastMoney = lnLastCoinMoney;  // 更新剩余金额lnLastZhang = varr[i];}}}}return lnTotalCoins;
}int main() {int result = BuyCola();cout << "总投币次数为: " << result << endl;return 0;
}

司机问题(动态规划)

题目

现在有司机N*2人 调度中心会将所有的司机平均分配给A B 两个区域
他们每个人去A B区域会获得不同的工资 (给定两个数组A B来表示他们可能的收入)
现在要求你所有的调度方案中能让所有司机总收入最高的方法

分析

如果之前看过我写的动态规划部分博客的同学看到这个问题就应该联想到从左到右的递归模型

我们只需要从左到右 列举出每一种可能性 这个问题就能迎刃而解了

具体思路如下

我们定义一个函数 process(index , rest)

其中index表示当前司机下表

rest表示A剩余能够分配的司机数(因为强制要求司机必须要一半一半)

所以说我们就能得到两种可能性

  1. 司机去A 之后加上 ``process(index+ 1, rest - 1)
  2. 司机去B 之后加上 ``process(index+ 1, rest )

之后只要不断地递归就可以了

代码详解

当然我们最后可以使用一个dp数组来完成记忆化搜索让该函数的时间复杂度尽可能的降低

int process(vector<int>& A, vector<int>& B, int index, int rest , vector<vector<int>>& dp) {if (index == A.size()){return 0;}if (rest == 0){return B[index] + process(A, B, index + 1, rest ,dp);}if (index + rest > A.size()){return A[index] + process(A, B, index + 1, rest , dp);}// 检查缓存if (dp[index][rest] != -1) {return dp[index][rest]; // 如果已有结果,则直接返回}// 选择去B区和去A区的最大收入int p1 = process(A, B, index + 1, rest, dp) + B[index]; // 司机去B区int p2 = process(A, B, index + 1, rest - 1, dp) + A[index]; // 司机去A区// 记录并返回最大收入dp[index][rest] = max(p1, p2);return dp[index][rest];
}

http://www.ppmy.cn/news/1539341.html

相关文章

PCL 渐进式形态学滤波

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 如果不太了解点云数学形态学的基本理论,可以先阅读这篇文章:https://blog.csdn.net/dayuhaitang1/article/details/123172437。形态学中的窗口结构一直存在着这样的问题:如果窗口结构元尺寸过小,则无法去除一些…

性能测试:流量回放工具-GoReplay!结合一款无需CA证书即可抓取HTTPS明文的工具,简直无敌

性能测试&#xff1a;流量回放工具-GoReplay&#xff01;结合一款无需CA证书即可抓取HTTPS明文的工具&#xff0c;简直无敌。 GoReplay 是一个开源网络监控工具&#xff0c;可以将实时 HTTP 流量捕获并重放到测试环境。 应用成熟的过程中&#xff0c;测试所需的工作量往往会成…

移动端响应式布局(媒体查询+rem,vw+rem,flexible.js+rem)

一、媒体查询 1.主要作用 检测设备大小是否发生改变&#xff0c;可以实现响应式布局。 2.响应式布局 会根据设备的大小显示出不同的布局效果&#xff0c;在大设备和小设备上显示的布局可能是不一样的。 要想知道设备大小的改变&#xff0c;需要借助于媒体查询去实现。 3.语法…

提升正则表达式性能:全面解析Golang regexp/syntax包

提升正则表达式性能&#xff1a;全面解析Golang regexp/syntax包 介绍基本概念正则表达式简介regexp/syntax包的作用 regexp/syntax包的结构核心组件结构详解ParserRegexpOpInstProg 使用Parser解析正则表达式解析正则表达式AST的结构 分析解析后的正则表达式树AST节点类型分析…

特惠电影票API接口的优势功能以及对接因素?

特惠电影票对接接口通常是指允许第三方开发者或平台通过编程方式接入电影票预订服务的API。这些接口可以提供查询电影场次、座位信息、票价、订票、支付等功能。以下是一些关键点和考虑因素&#xff0c;以及一些提供特惠电影票API接口的平台&#xff1a; 关键功能 电影场次信息…

【修订中】ffmpeg 知识点

一、两种安装方式 static FFmpeg binaries for macOS 64-bit Intel brew install ffmpeg 时间有点长 需要挂上代理 二、改

Spring Boot 开发详细案例:在线课程管理系统

目录: 项目概述开发环境与依赖配置项目结构设计数据库设计与配置业务逻辑层与数据访问层的实现Spring Security 权限管理控制器的实现前端交互与 API 测试总结1. 项目概述 本案例将构建一个 在线课程管理系统,功能包括用户注册、登录、课程的管理(增删改查),以及课程的分…

this指针—静态成员—单例模式

01 this指针 C是在C语言的基础上发展而来的&#xff0c;第一个C的编译器实际上是将C程序翻译为C语言&#xff0c;然后再使用C语言编译器编译 C语言中没有类的概念&#xff0c;只有结构&#xff0c;函数都是全局函数&#xff0c;没有成员函数的概念 翻译的时候&#xff0c;将cla…