6.23 deque | 优先队列_堆排序 | 博弈论
lc1753.移除石子
贪心
为了将分数最大化,每次将最大的两个数进行减少,(循环中sort),直到有两堆为零。
class Solution {
public:
int maximumScore(int a, int b, int c) {
int ans = 0;
vector<int> rec = {a, b, c};
sort(rec.begin(), rec.end());
while (rec[1] != 0) {
ans++;
rec[1]--; rec[2]--;
sort(rec.begin(), rec.end());
}
return ans;
}
};
将上面循环当中的sort方法,可以采取优先队列来维护。
priority_queue top pop push
class Solution {
public:
int maximumScore(int a, int b, int c) {
// 使用最大堆(优先队列)存储石子数量
priority_queue<int> maxHeap;
maxHeap.push(a);
maxHeap.push(b);
maxHeap.push(c);
int score = 0;
while (maxHeap.size() >= 2) {
// 取出最大的两堆
int first = maxHeap.top(); maxHeap.pop();
int second = maxHeap.top(); maxHeap.pop();
// 各取一个石子
first--;
second--;
score++;
// 如果还有石子,放回堆中
if (first > 0) maxHeap.push(first);
if (second > 0) maxHeap.push(second);
}
return score;
}
};
lc451. 频次排序
桶排序
class Solution {
public:
string frequencySort(string s) {
string res;
unordered_map<char, int> dict;
for (char c : s) ++dict[c];
vector<string> sub(s.size() + 1);
for (auto &[key, value] : dict)
sub[value].append(value, key);
//桶排序
for (int i = s.size(); i > 0; --i)
if (sub[i].size()) res.append(sub[i]);
return res;
}
};
堆排序
hash统计好频次后,v,k到priority_queue中,实现排序,然后再top pop取出
class Solution {
public:
string frequencySort(string s) {
string res;
unordered_map<char, int> dict;
for (char c : s) ++dict[c];
priority_queue<pair<int, char>> sub;
for (auto &[key, value] : dict)
sub.push({value, key});
//实现降序排序
while (sub.size()) {
res.append(sub.top().first, sub.top().second);
sub.pop();
}
return res;
}
};
lc292 博弈论
先后手
博弈论就是研究人们在互动决策时,如何根据他人可能的选择来调整自己的策略,以追求利益最大化的学问,就像下棋时你得琢磨对手下一步怎么走,再决定自己怎么落子。
总结:对于范围内判断
%(min+max)==0 那么就一定是对方赢
class Solution {
public:
bool canWinNim(int n) {
return n % 4 != 0;
}
};
典型博弈论,直接return true也行
class Solution {
public:
bool stoneGame(vector<int>& piles)
{
sort(piles.rbegin(),piles.rend());
int ali=0,bob=0;
for(int i=0;i<piles.size();i++)
{
if(i%2==0) ali+=piles[i];
else bob+=piles[i];
}
return ali>bob;
}
};
常见博弈论
- 必胜态和必败态:
比如和朋友轮流拿糖果,只剩1颗时你拿就赢(必胜态),但如果轮到你时只剩2颗且规则是每人最多拿1颗,你拿完对方就能拿最后1颗(你处于必败态)。
(本题就是%(min+max)后的拓展
- Nim游戏(取石子):
三堆石子分别有3、4、5颗,和朋友轮流拿,每次从一堆里拿任意数量。这时候算3⊕4⊕5=2≠0,说明先手能赢(比如先从5颗里拿2颗,让剩下的异或和为0,之后对方怎么拿你都按规则调整,最后必赢)。
- SG函数(玩跳格子):
每个格子有不同规则(比如从格子A能跳到B/C/D),SG函数就像给每个格子算“安全值”,把多个格子的SG值异或起来,不为0时先手能赢,本质是看你能不能跳到“安全格子”让对手陷入被动。
- 公平组合游戏:
类似“石头剪刀布”,两人规则一样,胜负只看双方选择,没有运气成分(但算法题里常是取物品这类有固定规则的博弈)。
博弈论的异或推论操作
priority_queue 优先队列
- 最大堆: priority_queue<T> pq;
- top最小堆: priority_queue<T, vector<T>, greater<T>> pq;
操作push_back,pop_back,top
lc884
优解:
将两个字符串合二为一,统计只出现一次的即可
最开始顺手写的,实际项目的话,可以解耦出cntWord,和compare函数
class Solution {
public:
vector<string> uncommonFromSentences(string s1, string s2) {
unordered_map<string,int> hash1;
unordered_map<string,int> hash2;
for(int i=0;i<s1.size();i++)
{
string tmp;
while(s1[i]!=' ' && i<s1.size())
{
tmp+=s1[i];
i++;
}
hash1[tmp]++;
}
for(int i=0;i<s2.size();i++)
{
string tmp;
while(s2[i]!=' ' && i<s2.size())
{
tmp+=s2[i];
i++;
}
hash2[tmp]++;
}
vector<string> ret;
for(auto& [a,b]:hash1)
{
if(b==1 && !hash2.count(a))
{
ret.push_back(a);
}
}
for(auto& [a,b]:hash2)
{
if(b==1 && !hash1.count(a))
{
ret.push_back(a);
}
}
return ret;
}
};
deque双端队列
c++数据结构可以头插,还可以下标访问元素
同时支持高效头插和下标访问的数据结构较少,但以下两种方案可满足需求(需权衡效率):
1. std::deque (双端队列)
- 头插支持:使用 push_front() ,时间复杂度 O(1)。
- 下标访问:支持 operator[] 和 at() ,时间复杂度 O(1)。
- 原理:内部通过分段动态数组实现,头尾插入无需移动全部元素,但下标访问时可能需要跨段寻址(效率略低于 vector ,但仍为常数时间)。
- 示例:
#include <deque>
std::deque<int> dq;
dq.push_front(10); // 头插
dq.push_front(20); // 现在 dq[0]=20, dq[1]=10
int first = dq[0]; // 下标访问
2. 自定义结构( list + map 组合)
- 方案:用 std::list 实现头插(O(1)),同时用 std::map<int, list::iterator> 记录下标与迭代器的映射,实现下标访问。
- 复杂度:
- 头插:O(1)( list 原生支持)。
- 下标访问:O(log n)( map 查找)。
- 示例(简化逻辑):
#include <list>
#include <map>
template <typename T>
class CustomStructure {
private:
std::list<T> data;
std::map<int, typename std::list<T>::iterator> indexMap;
public:
void push_front(const T& val) {
data.push_front(val);
indexMap[0] = data.begin(); // 头插后更新索引0
// 其他索引需后移,实际需遍历map调整,复杂度O(n),此处简化
}
T& operator[](int idx) {
return *indexMap[idx];
}
};
缺点:实现复杂,头插时维护索引映射需 O(n) 时间,仅适用于特定场景。
不推荐方案
- std::vector :头插需移动所有元素(O(n)),效率极低,仅尾插高效。
- std::list :不支持下标访问(需从头遍历,O(n))。
最优选择建议
- 若下标访问频率高且头插频率低,优先选 std::deque ,其综合性能更优;
- 若头插频率极高且必须下标访问,可考虑自定义结构,但需接受额外的时间和代码复杂度。
lc2899 deque双端队列 push_front
选对数据结构,做题就是很简单(=^・ω・^=)
class Solution {
public:
vector<int> lastVisitedIntegers(vector<int>& nums)
{
vector<int> ans;
deque<int> dq;
int n=nums.size();
int cnt=0;
for(int i=0;i<n;i++)
{
if(nums[i]>=0)
{
dq.push_front(nums[i]);
cnt=0;
}
else
{
cnt++;
if(cnt<=dq.size())
{
ans.push_back(dq[cnt-1]);
}
else
ans.push_back(-1);
}
}
return ans;
}
};
lc905 奇偶排序
deque
class Solution {
public:
vector<int> sortArrayByParity(vector<int>& nums)
{
deque<int> dq;
for(auto& n:nums)
{
if(n%2==0)
dq.push_front(n);
else
dq.push_back(n);
}
vector<int> ret;
for(auto& d:dq)
ret.push_back(d);
return ret;
}
};