第一百零二天 --- 力扣第 268 场力扣周赛
题目一
思路
1、这个题目很简单,就是拿出一个数,首先是十进制回文数,将之转化成为K进制之后,也是回文的,那么这个二进制数就是答案之一,这个题的思路就是这样。
2、但是,我们不能硬枚举从1开始的所有十进制数,这样一定会超时,因为你看样例,可能的十进制数会非常大,所以直接枚举十进制数肯定行不通,那么我干脆,直接从一个十进制回文数,推出下一个十进制回文数,这样的话,大幅减少了计算次数,降低了复杂度。
通过上一个回文数推下一个回文数
1、如何从一个十进制回文数推出下一个十进制回文数?
<1> 通过观察很多回文数,发现他们的变化规律是先动中间,后动两边。
<2> 当中间遇到9,就得进位。
<3> 因为是镜像,所以只需要每次生成前半部分,后半部分直接复制前半部分即可
2、特殊:当9999->10001时候,因为全都是9,所以整体进位,直接生成就行,1后面跟了4(这个数要看传进来的回文数长度)个零,最后保证回文,要加一。
3、传进来的数,长度为奇数偶数对结果影响很大,所以一定要区分,主要影响在中轴的位置。
进制转化
我代码中的有点问题,比如十进制2,会被转化成二进制 01,反过来了,但没关系,我们要的是回文数,所以正反都是一样的。
回文数比较
一定记住,直接用string比较最方便快捷,先反转字符串,反转前后一样的话,说明是回文的。
细节
因为数都很大,所以用long long 防止越界。
代码
class Solution {
public:
int k;
bool is_mirror(string str_k) {
string tmp(str_k);
reverse(tmp.begin(), tmp.end());
return tmp == str_k ? true : false;
}
string trans(long long item) {
string ans = "";
while (item > 0) {
ans += (item % k) + '0';
item /= k;
}
return ans;
}
long long next_mirror(long long mirror) {//手动产生下一个回文数
string str_mirror = to_string(mirror);
int n = str_mirror.size();
//通过观察,回文数的变化一定是先中间在两边,所以一切都从中间开始变化
int edge = (n % 2 == 0) ? n / 2 - 1 : n / 2;//奇偶数对中间的判定有影响
for (int i = edge; i >= 0; i--) {
if (str_mirror[i] != '9') {//定位到第一个不是9的,承受进位
str_mirror[i]++;
for (int j = i + 1; j <= edge; j++) {//为之前的9手动进位
str_mirror[j] = '0';
}
for (int j = edge + 1; j < n; j++) {//为后半段手动镜像
str_mirror[j] = str_mirror[n - j - 1];
}
return stoll(str_mirror);//转换成long long 返回
}
}
//特殊情况,全都是9,集体进大位!!!!!!!
long long ans = 1;
for (int i = 0; i < n; i++) {
ans *= 10;
}
return ans + 1;
}
long long kMirror(int k, int n) {
this->k = k;
long long ans = 0;
long long num = 0;
long long mirror = 1;
while (n > 0) {
string trans_k = trans(mirror);
if (is_mirror(trans_k)) {
ans += mirror;
n--;
}
mirror = next_mirror(mirror);
num++;
}
return ans;
}
};
所有代码均以通过力扣测试
(经过多次测试最短时间为):
题目二
思路
这个题就思路就更简单了,每一次查询就在给定的查询范围内,查找对应值出现次数就行了。但是请看本题数据规模,查询次数10万次,整体数据量10万,那么这个题就不简单了,我们就得想办法降低查询的复杂度!!!!!
记忆化处理
这个很好想,既然查询次数很多很多,那么我们就不能做重复查询的事情,所以,直接每次查询之前先看看之前查过了吗,没查过就查,最后别忘了记录。
记录位置+二分查找优化查询过程
那么,还有什么可以优化的呢?很显然,我们统计区间内目标出现次数的方法太笨了。
1、我们老套的办法,是枚举所有的位置,看该位置对应的值是不是想要的。
2、但那我们为什么不记下要查找的东西的全部的位置,反其道而行之(必须从前到后记录,保证位置严格递增),用已知边界去匹配上面的位置,看已知边界能包住多少次出现记录,再用二分法找到距离边界最近的左右出现位置(要求就是出现的位置在给定区间里且最靠近左右边界),这样我们就能找到符合题目要求的数,最开始在第几次时候出现,最后是在第几次时候出现,二者做差加一,即为答案。这样,平均每次查询,复杂度都在O(lgN)左右,就降低了复杂度。
3、这种方法极为重要,可以将区域内查询某种元素个数复杂度下降到O(lgN)左右,是一种优化方案,我们在用二分法匹配好左右边界之后,剩下的操作就要看具体的题目要求了
<1> 比如力扣5900. 蜡烛之间的盘子这道题中,我们用上述方法确定了两根蜡烛之后,要看求什么,人家要求这两根蜡之间有多少盘子,所以有涉及区间和,所以在用前缀和法快速求解
<2> 回到本题,我们匹配到两个边界位置之后,知道了第一次符合要求的是第几次目标出现,最后一次符合要求的是第几次目标出现,二者相减+1,在O(1)内找到答案
所以匹配到了符合的左右边界之后具体怎么做要看题意。
细节
1、在做记忆化的时候,需要存三元组和哈希表,这里用的map,用结构体定义了三元组,值得注意的是map需要内置排序函数,所以要给出结构体的排序方法,这里随便给,但是要注意一定要把这三个元素都比进去,要不然比如right 和value没进行比较,map就会认定二者是一样的,在.count({1,0,1}) 和 .count({1,1,0})的时候就会认为二者一样。
2、在查找左右边界时候,一定要注意:
<1> 为啥想到二分法。不论你是查找某一个值,还是左右边界,只要是跟查找有关系,建议都先首选二分。
<2> 不管具体题意咋变,二分法核心就是,确定左右边界,每次找到中间,看是否查找成功,成功了根据题意写怎么办,不成功了再根据题意写咋办,这种核心思想不会变。
<3> 本题中的二分和力扣5900. 蜡烛之间的盘子一致,请读者看我的那篇博客学习,这里不作赘述。
代码
struct record {//三元组
int left;
int right;
int value;
bool operator < (const record & r) const {//随便写比较方法,,但是三个元素得写全
if (left == r.left&& right == r.right) {
return value < r.value;
}
else if (left == r.left) {
return right < r.right;
}
return left < r.left;
}
};
class RangeFreqQuery {
public:
RangeFreqQuery(vector<int>& arr) {
this->arr = arr;//存下来arr
for (int i = 0; i < arr.size(); i++) {//预处理,存一下每个元素出现的位置信息
item[arr[i]].push_back(i);
}
}
int query(int left, int right, int value) {
if (items.count({ left,right,value }) > 0) {//记忆化
return items.at({ left,right,value });
}
if (left == right && value == arr[left]) {//特殊情况直接处理
return 1;
}
if (item.count(value) == 0) {//不存在的元素
return 0;
}
vector<int> tmp = item[value];//该元素存在,取出出现的路径信息
int n = tmp.size();
if (left > tmp[n - 1]) {//排除掉直接错误情况
return 0;
}
if (right < tmp[0]) {//排除掉直接错误情况
return 0;
}
this->tmp = tmp;//进行赋值,准备查询
this->left = left;
this->right = right;
if (find(false) == -1 || find(true) == -1) {//只有左右边界都存在,才有非零解
items[{ left, right, value }] = 0;
return 0;
}
items[{ left, right, value }] = find(false) - find(true) + 1;//右边界次数-左边界次数+1
return items[{ left, right, value }];
}
int find(bool is_left) {//参数为true为左边界,反之右边界
int i = 0, j = tmp.size() - 1, ans = -1;//这里的tmp是该元素全部出现的位置序列
while (i <= j) {
int mid = (i + j) / 2;
if (tmp[mid] >= left && tmp[mid] <= right) {//该位置在给定的区间,即成功情况
ans = mid;
if (is_left) {//向左走
j = mid - 1;
}
else {
i = mid + 1;
}
}
else if (tmp[mid] > right) {//失败情况
j = mid - 1;
}
else {//失败情况
i = mid + 1;
}
}
return ans;//返回左右边界在tmp中下标,代表出现次数
}
private:
map<record, int> items;//用于记忆化
unordered_map<int, vector<int>> item;//存储 值-出现位置序列
vector<int> arr;//存数组
vector<int> tmp;//存某一个值对应的位置序列
int left;//左边界
int right;//右边界
};
所有代码均以通过力扣测试
(经过多次测试最短时间为):
题目三
力扣:2079. 给植物浇水
思路
这个题很简单,直接模拟就行。
1、每次向前走一步,如果可以浇水,就把水量减下去,答案+1,位置+1
2、发现此时交不了水,就退回去取水,用(pos-1)*2步,回来再浇水即可。
代码
(这个题你就按照我说的自己在纸上画一遍就懂了,很简单的模拟题)
class Solution {
public:
int reload_water(int pos) {//取水步数
int step = pos + 1;
return step * 2;
}
int wateringPlants(vector<int>& plants, int capacity) {
int ans = 0;
int pos = 0;
int n = plants.size();
int tmp_capacity = capacity;//初始化,单次水量
while (pos < n) {
if (tmp_capacity >= plants[pos]) {//能浇水
tmp_capacity -= plants[pos];
pos++;//位置++
ans++;
}
else {
ans += reload_water(pos - 1);//回去取水
tmp_capacity = capacity;//水量回满
}
}
return ans;
}
};
所有代码均以通过力扣测试
(经过多次测试最短时间为):
题目四
思路
直接模拟+暴力破解即可
代码
class Solution {
public:
int maxDistance(vector<int>& colors) {
int ans = 0;
for (int i = 0; i < colors.size(); i++) {//直接枚举,任意一个房子作为起点
for (int j = i+1; j < colors.size(); j++) {//找它后面的放子
if (colors[i] != colors[j]) {
ans = max(ans, j - i);//找最大的
}
}
}
return ans;
}
};
所有代码均以通过力扣测试
(经过多次测试最短时间为):