对顶堆
使用一个最大堆和一个最小堆,最大堆维护小于中位数的那一半数,最小堆维护大于中位数的那一半,然后 要保持两个堆的大小平衡,即数量差值小于等于1,当数字总数是奇数的时候,中位数就是数字数量较大堆的堆顶元素,当总数为偶数的时候,中位数就是两个堆顶元素的平均值。
priority_queue<int,vector<int>,less<int>>que1 ;//最大堆存小于中位数的部分
priority_queue<int,vector<int>,greater<int>>que2;//最小堆存大于中位数的部分
void addNum(int num) {
if (que1.empty() || num <= que1.top()) {
que1.push(num);
if (que2.size() + 1 < que1.size()) {
que2.push(que1.top());
que1.pop();
}
} else {
que2.push(num);
if (que2.size() > que1.size()) {
que1.push(que2.top());
que2.pop();
}
}
}
巴什博弈
(博弈论的一种问题情形)
只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。只要 n 是(m+1)的倍数,那么 先手者就必输。反之,后者必赢。
类似题目:
1. 作为计算机学院的学生,Kiki和Cici打牌的时候可没忘记专业,她们打牌的规则是这样的:
-
1、 总共n张牌;
-
2、 双方轮流抓牌;
-
3、 每人每次抓牌的个数只能是2的幂次(即:1,2,4,8,16…)
-
4、 抓完牌,胜负结果也出来了:最后抓完牌的人为胜者
这个可以dp
这类题目可以先枚举几个初始的状态,然后基本就可以找到规律了,本质还是动态规划,但是规划之后会有非常明显的循环,所以才会得出倍数关系
我们可以先列个表
剩余牌数 | 先手必胜T/必输F |
---|---|
1 | T |
2 | T |
3 | F |
4 | T |
5 | T |
6 | F |
7 | T |
8 | T |
9 | F |
10 | T |
11 | T |
12 | F |
从上面可以逐渐看到,当剩余牌数为3的倍数,先手必输。我们可以逐步的看这个过程,首先1和2很明显可以直接拿走所有的牌而获胜,但是对于3,无论是拿走1或2,都会将先手必赢的局面让给对方,根据这个思路,我们可以发现,剩余牌数为4的情形下,获胜的方法就是把必输的局面让给对方,进一步,剩余牌数为n时,获胜的方法,就是枚举n-2^k ( k=0,1,2,3…),当我们能够找到一个局面使下次对面先手的时候必输,那么我们就是必赢的,否则我们必输。最后dp的结果就是发现循环。
前K大/小个元素
这个最简单的思路就是排序后直接查找,但是排序的时间复杂度最低为nlogn
,这里介绍一种时间复杂度为n
的方法。
以前K大元素为例,我们建立一个小根堆,当堆不满时,将当前元素插入堆中,当堆满时,比较当前元素与堆顶元素的大小,如果当前元素大于堆顶元素,则将堆顶元素弹出,将当前元素插入堆中,否则,不做任何操作。原理是,使用了小根堆,保证堆顶元素一定是其中最小的。对于前K大元素,它最多比K-1个元素要小,但是假如当前元素比堆顶元素小,它就已经小于K个元素了,所以它一定不会是前K大个元素。所以其实堆中存放的是遍历到当前位置之前可能的前K大元素,当我们遍历完所有元素之后就是最终的结果了。
一个实例代码,
构造堆
class Solution {
public:
static bool cmp(pair<int, int>& m, pair<int, int>& n) {
return m.second > n.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> occurrences;
for (auto& v : nums) {
occurrences[v]++;
}
// pair 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
for (auto& [num, count] : occurrences) {
if (q.size() == k) {
if (q.top().second < count) {
q.pop();
q.emplace(num, count);
}
} else {
q.emplace(num, count);
}
}
vector<int> ret;
while (!q.empty()) {
ret.emplace_back(q.top().first);
q.pop();
}
return ret;
}
};
链接:https://leetcode.cn/problems/top-k-frequent-elements/solutions/402568/qian-k-ge-gao-pin-yuan-su-by-leetcode-solution/
基于桶排序思想
还是先使用哈希表统计出现的频率,不同之处在于使用桶来找到前K个高频元素,由于数组长度为n,所有元素的最大出现频率为n,所以说啊,我们可以建立长度为n的桶,每个桶的序号对应出现频率减一,先遍历一次所有元素,然后对这个桶反向遍历,直到已经找到K个元素或者遍历完桶。
字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
代码:
class Solution {
public:
string getDigits(string &s,size_t &ptr){
string ret ="";
while(isdigit(s[ptr])){
ret.push_back(s[ptr++]);
}
return ret;
}
string getString(vector<string>&v){
string ret;
for(const auto &s:v){
ret+=s;
}
return ret;
}
string decodeString(string s){
vector<string>stk;
size_t ptr=0;
while(ptr<s.size()){
char cur=s[ptr];
if(isdigit(cur)){
string digits=getDigits(s,ptr);
stk.push_back(digits);
} else if(isalpha(cur)||cur=='['){
stk.push_back(string(1,s[ptr++]));
}
else{
++ptr;
vector<string> sub;
while(stk.back()!="["){
sub.push_back(stk.back());
stk.pop_back();
}
reverse(sub.begin(),sub.end());
stk.pop_back();
int repTime=stoi(stk.back());
stk.pop_back();
string t,o=getString(sub);
while(repTime--)t+=o;
stk.push_back(t);
}
}
return getString(stk);
}
};
总之这题思路很好理解,使用栈来进行处理,构造了两个辅助函数,string getDigits(string &s,size_t &ptr)
和string getString(vector<string>&v)
,前者用于在输入字符串中截取一个连续的数字、同时更新指针ptr,后者用于用于从一个vector<string>
向量中获取完整的字符串(这里其实就是在整合)。主要是这题频繁的压栈与弹出操作比较复杂,需要仔细理清楚过程。这里用到了两个<cctype>
头文件下的方法,isdigit( )
和isalpha( )
分别用于判断输入的字符是数字还是其他字符。