算法提升二
- [最大分配工资问题 (贪心)](#最大分配工资问题 (贪心))
- [数组有序问题 (贪心)](#数组有序问题 (贪心))
- [消息流问题 (数据结构设计)](#消息流问题 (数据结构设计))
- [可乐问题 (Coding能力)](#可乐问题 (Coding能力))
- 司机问题(动态规划)
最大分配工资问题 (贪心)
题目
给定数组Hard和Money 长度都为N
Hard[i]表示i号的难度 Money[i]表示i号工作的收入
给定数组ability 长度都为M ability [i] 表示i号人的工作能力
没一号工作都可以提供无数的岗位 难度和收入都一样 但是人的能力必须大于等于这份工作的难度 才可以上班
现在要求返回一个长度为M的数组ans ans[i]表示i号能获得的最好收入
分析
对于这道题目来说 最简单能想到的方法就是遍历整个难度收入数组 就可以找到最高的收入
如果我们想要获得最好的收入我们就需要想到这样的一个贪心点
难度 - 收入 必须要呈正相关 不然一个难度更高收入更低的工作谁会去做呢
有了这个大前提之后我们便可以将难度收入数组排序
- 同等难度下 我们只要收入最高的
- 难度越高 收入必须越高
这样子我们就能得到一个数组(当然 为了比较方便 我们也可以使用map来存储)
之后只需要使用二分查找 或者使用map的upper_bond函数就可以找到每号人的最好收入了
代码详解
cpp
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;
}
}
cpp
sort(vJob.begin(), vJob.end(), Compare);
我们首先写出一个比较函数来job进行排序 (难度小的工作排在难度大的工作前面 同等难度的工作收入高的排在前面)
cpp
// 之后我们创建一个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的一个迭代器
cpp
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号)
我们观察可以发现 这两个位置圈起来的子数组排序下 刚好就是我们需要的答案
原理 (默认顺序)
因为顺序数组一定有前面的数字小于等于后面的数字 后面的数字大于等于前面的数字
所以说对于一个不是顺序结构的数组 我们只需要通过上面的方法找到这两个边界便可以将其变有序了
代码详解
cpp
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 头尾相连 我们打印整个单链表就能得到我们想要的结果了 (当然我们要记得处理下头尾表数据)
代码详解
我们首先设计一个信息流节点
cpp
struct Node
{
int val; 代表节点的编号 我们使用该编号来排序
char c; 代表节点传输的信息
Node* next; next指针
};
cpp
map<int, Node*> mapHead = {};
map<int, Node*> mapTail = {};
int WantNum = 1; // 我们现在想要的数字
接下来设计下头尾表和我们想要的数字
注意!!!! 在C++中的map和Java中的map不一样 如果我们定义map<int, Node> mapHead
那么穿进去的Node只会是一个拷贝值 他俩的指针没有任何关系 但是在Java中 他们就是一种引用关系
所以说在C++中我们要使用指针
接下来完整代码如下
cpp
// 函数:将节点放入链表
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能力 也就是我们如何将一个实际问题转化为工程问题的能力
我们对问题进行分析
首先让我们用大面值的面额的纸币去购买一瓶可乐
这里就出现了三种情况
- 比当前面值大的纸币还没有用完 需要用当前纸币补上零钱
- 当前纸币足够买x瓶可乐 这里就要考虑找零问题
- 当前纸币没有了或者说剩下的额度不足以购买一瓶可乐了 此时我们就要考虑使用下个额度的纸币
转化为代码风格如下
cpp
// 1. 如果前面有剩下的零钱我们要把他用上
int lnLastMoney = 0;
int lnLastZhang = 0;
go
// 使用公式进行计算即可
go
lnLastMoney += // 剩下的总钱数
lnLastZhang += // 剩下的总张数
代码详解
整体代码表示如下
cpp
// 定义初始硬币数量
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, 10
vector<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剩余能够分配的司机数(因为强制要求司机必须要一半一半)
所以说我们就能得到两种可能性
- 司机去A 之后加上 ``process(index+ 1, rest - 1)
- 司机去B 之后加上 ``process(index+ 1, rest )
之后只要不断地递归就可以了
代码详解
当然我们最后可以使用一个dp数组来完成记忆化搜索让该函数的时间复杂度尽可能的降低
cpp
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];
}