一.双指针
1.基本介绍
双指针算法是一种暴力枚举的优化算法,他也被叫做尺取法或者滑动窗口。
当我们发现算法需要两次for循环时并且两个指针可以不回退,我们可以利用双指针来优化算法复杂度。
2.例题详解
题目描述
企业家 Emily 有一个很酷的主意:把雪花包起来卖。她发明了一台机器,这台机器可以捕捉飘落的雪花,并把它们一片一片打包进一个包裹里。一旦这个包裹满了,它就会被封上送去发售。
Emily 的公司的口号是"把独特打包起来",为了实现这一诺言,一个包裹里不能有两片一样的雪花。不幸的是,这并不容易做到,因为实际上通过机器的雪花中有很多是相同的。Emily 想知道这样一个不包含两片一样的雪花的包裹最大能有多大,她可以在任何时候启动机器,但是一旦机器启动了,直到包裹被封上为止,所有通过机器的雪花都必须被打包进这个包裹里,当然,包裹可以在任何时候被封上。
输入格式
第一行是测试数据组数 T,对于每一组数据,第一行是通过机器的雪花总数 n(n <= 1e6),下面 n 行每行一个在 [0,1e9] 内的整数,标记了这片雪花,当两片雪花标记相同时,这两片雪花是一样的。
输出格式
对于每一组数据,输出最大包裹的大小。
输入输出样例 #1
输入 #1
1
5
1
2
3
2
1
输出 #1
3
我们在暴力枚举的过程中,当我们第一次将right变量扫描到重复字符时,我们会发现,right此时不能继续向后遍历,left需要往前,而left往前时right也不需要回退,所以我们可以依据此来优化算法。
首先初始化left与right指针指向0位置,开始遍历数组,直到right等于数组大小时停止,用哈希表来存储出现过数的次数,当right遇到重复时,left往前++,并且哈希表对应值减1,最后更新窗口,找出最大值即可。
下面是代码样例:
cpp
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
int main()
{
int T; cin >> T;
while(T--)
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
// 初始化
int left = 1, right = 1, ret = 0;
unordered_map<int, int> mp; // 维护窗⼝内所有元素出现
while(right <= n)
{
// 进窗⼝
mp[a[right]]++;
// 判断
while(mp[a[right]] > 1)
{
// 出窗⼝
mp[a[left]]--;
left++;
}
// 窗⼝合法,更新结果
ret = max(ret, right - left + 1);
right++;
}
cout << ret << endl;
}
return 0;
}
3.总结
从上文我们可以得知当我们需要进行暴力枚举并且在两层for循环中的指针不会回退时,我们可以采取此方法来优化算法。
算法的模板大致为,首先对窗口进行初始化,创建一个窗口来维护数据,接下来将元素进窗口,用哈希表统计次数,判断当位置的值超过1时,窗口内子串不合法,此时left所指元素需要进行出窗口处理,相应哈希表值减减,最后判断结束更新窗口。
二.二分算法
1.基本介绍
二分算法就是通过对一个有序的数组进行不断的二分缩小,每次缩小一半,来压缩出最终的结果,取数组中间的元素,若目标元素在中间元素的左边则将right移动到mid上,反之将left移动到mid上,他的模板相对简单,难点在于如何确定边界处理。
2.二分查找
当我们解决算法题时,发现该题的解具有二段性,即可以明确划分出具有两个性质的不同边界时,我们应该考虑使用二分算法。
(1)例题详解
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:
nums = [], target = 0
输出:[-1,-1]
提示:
• 0 <= nums.length <= 1e5
• -1e9 <= nums[i] <= 1e9
• nums 是一个非递减数组
• -1e9 <= target <= 1e9
如该题让我们找指定数的开始位置和结束位置,而且数组是有序的我们可以考虑使用二分算法。
这里我们要卡住左右端点,令left指向0,right指向末尾,当left没有相遇时不断遍历。我们先将mid设为(left+right)/2,当mid元素大于等于目标元素时,right移动到mid位置,否则left移动到mid+1的位置,此时我们回看,mid的取值,当我们取左端点时不会出现死循环的情况(切记要检查)。
然后我们存储左端点,按照此方法再把右端点算出来即可。
下面是代码样例:
cpp
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
vector<int> searchRange(vector<int>& nums, int target)
{
int n = nums.size();
if (n == 0)
{
return { -1,-1 };
}
int left = 0, right = n - 1;
while (left < right)
{
int mid = (left + right) / 2;
if (nums[mid] >= target)
{
right = mid;
}
else
{
left = mid + 1;
}
}
if (nums[left] != target)return { -1,-1 };
int retleft = left;
left = 0; right = n - 1;
while (left < right)
{
int mid = (left + right + 1) / 2;
if (nums[mid] <= target)
{
left = mid;
}
else
{
right = mid - 1;
}
}
return { retleft,left };
}
};
(2)总结
二分查找即在有序的数组中通过中间值大小不断进行范围压缩找到最终值。
下面是大致写题模板
cpp// ⼆分查找区间左端点 int l = 1, r = n; while(l < r) { int mid = (l + r) / 2; if(check(mid)) r = mid; else l = mid + 1; } // ⼆分结束之后可能需要判断是否存在结 果 // ⼆分查找区间右端点 int l = 1, r = n; while(l < r) { int mid = (l + r + 1) / 2; if(check(mid)) l = mid; else r = mid - 1; } // ⼆分结束之后可能需要判断是否存在结 果
3.二分答案
二分答案用于处理部分最大值最小,以及最小值最大的问题。如果解是从小到大变化的过程中,那么我们可以通过二分来判断最优解。
(1)例题详解
P2440 木材加工
题目背景
要保护环境
题目描述
木材厂有 n 根原木,现在想把这些木头切割成 k 段长度均为 l 的小段木头(木头有可能有剩余)。
当然,我们希望得到的小段木头越长越好,请求出 l 的最大值。
木头长度的单位是 cm,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。
例如有两根原木长度分别为 11 和 21,要求切割成等长的 6 段,很明显能切割出来的小段木头长度最长为 5。
输入格式
第一行是两个正整数 n,k,分别表示原木的数量,需要得到的小段的数量。
接下来 n 行,每行一个正整数 Li,表示一根原木的长度。
输出格式
仅一行,即 l 的最大值。
如果连 1cm 长的小段都切不出来,输出 `0`。
输入输出样例 #1
输入 #1
3 7
232
124
456
输出 #1
114
说明/提示
数据规模与约定
对于 100% 的数据,有1≤n≤1e5,1≤k≤1e8,1≤Li≤108(i∈[1,n])。
我们设切成的长度为x,切成的段数为c,我们可以发现。
当x增大时,c在减少。x减小时c增大。当x小于等于ret(最终结果)c大于等于k,也就是要切长度小于等于最优长度切出来的段数大于等于k。当x大于ret时,c小于k,也就是要切长度大于最优长度时,最终切出来段数小于k。
下面是代码样例:
cpp
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL a[N];
LL n, k;
LL calc(int x)
{
LL ret = 0;
for (int i = 1; i <= n; i++)
{
ret += a[i] / x;
}
return ret;
}
int main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
int left = 0, right = 1e8 + 10;
while (left < right)
{
int mid = (left + right + 1) / 2;
if (calc(mid) >= k)//left越大calc得到的越小,长度越长段数越小
{
left = mid;
}
else
{
right = mid - 1;
}
}
cout << left << endl;
return 0;
}
(2)总结
二分答案是基于二分查找的一种算法思路,常用语求解最优问题。对于一个具有单调性的问题,我们先确定答案范围left与right,在区间内进行二分取中间值,检查mid是否满足条件,最后更新区间继续二分直到找到最优解。