求最长回文子串的长度
Leetcode: https://leetcode.com/problems/longest-palindromic-substring/
ACWing: https://www.acwing.com/problem/content/141/
解法:
- 暴力搜索+判断 O(n^3) — 显然不好
- O(n^2) 中心拓展,动态规划
- O(n) kmp算法
比较容易想的是中心扩展的方法,思路是:依次枚举N个点为回文子串的中心,然后从中心往两边拓展,找到以当前点为中心能得到的最大回文子串长度,最后取最大。
这个思路还需要解决一个奇偶问题(eg: aba, abba),常见的策略是在每两个字符中间插入一个没出现过的字符,比如‘#’,这样就可以把左右情况都转换为奇数的情况,减小代码实现难度。这也是解决奇偶问题常见的技巧。
这题还有个常见的错误解法:把当前串reverse之后去两个串的最长公共子串。反例:abcdba, abdcba。原数组最大回文子串的长度应该为1,按照上述方法求出来结果是2.
#include <string.h>
#include <iostream>
#include <algorithm>
using namespace std;
int get_longest_palindromic(string s){
int len = s.size();
s.resize(2 * len + 1); // 重置string的长度
for (int i = 2 * len; i >= 0; i--){
if (i % 2 == 0) s[i] = '#';
else s[i] = s[i / 2];
}
len = s.size();
int mv = 1;
for (int i = 0; i < len; i++){
int l = i - 1, r = i + 1;
int cnt = 1;
while(l >= 0 && r <= len){
if (s[l--] == s[r++]) cnt += 2;
else break;
}
mv = max(mv, cnt);
}
return mv >> 1; // 要除2
}
int main(){
string s;
string end = "END";
int len = s.size();
int num = 1;
while(cin >> s && s.compare(end)){
cout << "Case " << num++ << ": " << get_longest_palindromic(s) << endl;
}
return 0;
}
以上代码在测试样例很长时间,有超时风险。
常用代码小结
- 重置长度
将长度为N的序列每两个字符间插入‘#’,首尾也插入,查完之后长度为2 * N + 1.
** vector数组 **
for (int i = len - 1; i >= 0; i--){
chars.insert(chars.begin() + i + 1, '#'); //一定要加1嗷~
}
chars.insert(chars.begin(), '#');
** char[]数组 ** 初始化一旦定义了长度不能再更改,因此需要在初始化时将大小开成 2*N + 1
** char* ** 不好操作~~
** string **
s.resize(2 * len + 1); // 重置string的长度
for (int i = 2 * len; i >= 0; i--){
if (i % 2 == 0) s[i] = '#';
else s[i] = s[i / 2];
}
- 字符串比较
**比较两个字符串(string)是否相等不能用==, ==比较的是两个字符串地址是否相等**
string str1;
string str2;
str1.compare(str2); // 相等时返回0
char* str1;
char* str2;
int res = strcmp(str1, str2); //相等时返回0
/*
int strcmp(const char* s1,const char* s2)
当s1<s2时,返回为负数;
当s1==s2时,返回值= 0;
当s1>s2时,返回正数。
*/
折叠(旋转)数组中查找某个数
ACWing: https://www.acwing.com/problem/content/20/
解法:
- 循环找到折叠(旋转)位置,然后二分查找指定数
面试官提示:循环查找效率低,改成二分查找折叠(旋转位置),即二分查找数组中第一个最小的数(或者最后一个最大的数),剑指offer原题哎~~ (黑体部分是快速解题的关键,面试的时候是通过判断当前数比它后一个数大则返回,否者递归查找两边)
int get_fold_point(vector<int> nums){
int len = nums.size();
if (!len) return -1;
// 删除末尾和头结点相同的数,以便二分
int n = len - 1;
while(n >= 0 && nums[n] == nums[0]) n--;
if (nums[n] > nums[0]) return 0; //边界
int l = 0, r = n;
//这里不能直接调用half_check,对于边界的判断正好相反
// 一个是二分找到最小值,一个是二分找到某个值,判断条件不一样
while(l < r){
int mid = (l + r) >> 1;
if (nums[mid] < nums[0]) r = mid;
else l = mid + 1;
}
return l;
}
int half_check(vector<int>nums, int l, int r, int target){
while(l < r){
int mid = (l + r + 1) >> 1;
if (nums[mid] > target) r = mid -1;
else l = mid;
// 效果相同
// int mid = (l + r) >> 1;
// if (nums[mid] >= target) r = mid;
// else l = mid + 1;
}
return l;
}
int findMin(vector<int>& nums) {
int len = nums.size();
int target = nums[len / 3];
int index = get_fold_point(nums);
// 二分查找target
if (index == 0) return half_check(nums, 0, len - 1, target) ;
else if (target < nums[0]) return half_check(nums, index, len - 1, target);
else return half_check(nums, 0, index - 1, target);
}
以上代码有几个需要注意的点:
- 二分查找对折点时,要保证后面一段是单调递增才能用二分,因此需要将末尾和收割元素相等的元素都删掉。
- 删掉之后也有边界条件需要判断。边界条件就是删完之后末尾没有比第一个元素小的元素了,这时应该要返回数组中首个元素,否则会wa。
- 关于二分查找模板(https://www.acwing.com/blog/content/31/), 计算mid要不要加1,是根据判断条件最终写出来的样子判断的,判断条件用if (a < b) 和if(a <= b)最终的效果是一样的,这一点验证过了。
- 二分模板不能随便调用,要判断当前二分条件和单独的二分函数所假设的条件是否一样,若不一样,是不能直接调用的,如get_fold_point函数中的二分查找就和half_check中的判断条件不一样,因此不能直接调用half_check函数。