双指针
两大类:
1.两个指针指向同一个序列
2.两个指针指向不同的序列
通用模板:
for(int i = 0, j = 0;i < n;i++){
//chechk()不一定要明写出来,它只是表示满足某种性质
while(i < j && check(i, j)) j++;
//每道题目的具体逻辑
}
双指针最核心的用途就是优化
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
O(n^2)
}
}
双指针可以将上面的朴素算法优化到O(n)
例题:799.最长连续不重复子序列
给定一个长度为 n 的整数序列,请找出最长的不包含重复数字的连续子序列,输出它的长度。
输入格式
第一行包含整数 n。
第二行包含 n 个整数(均在0~100000范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复数字的连续子序列的长度。
数据范围
1<=n<=100000
输入样例
5
1 2 2 3 5
输出样例
3
————————————————————————————————————————————————
3 是 [2 3 5] 的长度
先考虑暴力解法,枚举所有可能的子序列,i 是子序列的尾,j 是子序列的头
for(int i = 0;i < n;i++){
for(int j = 0;j <= i;j++){
if(check(i, j)){
res = max(res, i - j + 1)
}
}
}
如何优化?找一下 i 和 j 的规律(一般是单调性),i 依然指向子序列的尾,j 则代表从 i 处往左走可以走到的最远位置,比如 [1 2 2],i 指向 2,则 j 也只能指向2,因为 j 再往左走,则子序列 [j—i] 就会包含重复数字了 ,假定 [j—i] 已经是没有重复数字的子序列了,则 i 后移一位产生的新的子序列,如果有重复的数字,那么一定是 a[i],那么 i 后移一位,会对 j 产生什么影响呢?两种情况:
1.移动后的 i ,与 [j—i-1] 范围内的某个数字相同,此时 j 要寻找新的合法子序列的头
2.移动后的 i ,与 [j—i-1] 范围内的所有数字不相同,此时 j 不用动
如何检测新的 a[i] 是否与 [j—i-1]的某个数字是否相同呢?
因为序列中的数最大10万,我们开一个 s 数组,记录当前子序列中每个数出现的次数,i 后移一位,产生了新的子序列,对于新的 a[i],更新 s[a[i]]++,只要 s[a[i]] > 1,就说明在新的子序列中,a[i] 这个数是重复的,那就从 j 开始不停的往后移动 j ,注意先更新 s[a[j]]- -
这一步就相当于把当前子序列中的所有数一一映射到了 s[ ] 中,本质上相当于哈希表
(如果数字远远大于10万,那就用哈希表)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
static int N = 100010;
static int n;
static int[] a = new int[N];
static int[] s = new int[N];//s[i]表示当前子序列中 i 这个数出现的次数
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader((System.in)));
n = Integer.parseInt(br.readLine());
String[] s1 = br.readLine().split(" ");
for(int i = 0;i < n;i++) a[i] = Integer.parseInt(s1[i]);
int res = 0;
for(int i = 0, j = 0;i < n;i++) {
s[a[i]]++;
//只要a[i]在当前子序列中的次数>1,就说明a[i]是重复的
while(s[a[i]] > 1) {//i < j可以省略,因为i每次都在++,而j是有条件的++
//所以天然的 j<=i,而最差情况直到j = i 时s[a[i]]=1,说白了就是加不加 j<i效果一样
s[a[j]]--;//将j指向的数踢出子序列
j++;//j就往后移
}//循环结束,j 指向第一个以i为尾的不重复子序列的头,多余的a[i]都被踢了
res = Math.max(res, i - j + 1);
}
System.out.println(res);
}
}
位运算
只讲最常用的两种操作
-
一个整数 n 的二进制表示中第 k 位是几(最低位是第0位)
(n >> k) & 1 即可
右移几位就是取第几位 -
lowbit(x)
返回 x 的最后一位 1---------> lowbit(x) = x&(-x)
比如 :
x = 1010,则 lowbit(x) = 10
x = 101000,则 lowbit(x) = 1000
原理:
-x = ~x+1
所以 x&(-x) = x&(~x + 1)
例子:
x = 1 0 1 0 … 1 0 0 … 0
~x= 0 1 0 1… 0 1 1… 1
~x+1=0 1 0 1… 1 0 0 … 0
x&(~x+1)= 1 0 0 … 0
————————————————————————————————————————————————
例题:LeetCode–191. 位1的个数
方法1:
public class Solution {
public int hammingWeight(int n) {
int count = 0;
while(n != 0){
n = n & (n - 1);
count++;
}
return count;
}
}
方法2:
public class Solution {
public int hammingWeight(int n) {
int count = 0;
while(n != 0){
n -= lowbit(n);//减去最后一位1,与方法1效果一样
count++;
}
return count;
}
public int lowbit(int x){
return x & (-x);
}
}
x = 1010 | |
---|---|
原码 | 00…0…1010 |
反码(x取反) | 11…1…0101 |
补码(x取反+1) | 11…1…0110 |
离散化
特指整数的离散化
一个整数序列:值域比较大,个数比较小
比如有1万个数,最小是 0,最大是 1亿,我们开不了长度一亿的数组,就需要对这 1 万个数进行离散化,把这 1 万个数映射到 0—n-1的连续的自然数(下标)上,就是把值映射成了下标。
例子:
a[ ] : 1, 3, 100, 2000, 500000 (a 是有序的)
把这5个数分别映射到 0, 1, 2, 3, 4(下标)
问题:
1.a 中可能有重复元素------>需要去重
2.如何算出序列中某个值 x 离散化后的值是多少 ?因为序列有序,所以二分
————————————————————————————————————————————
例题:802.区间和
假定有一个无限长的数轴,数轴上每个坐标的数都是0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l , r] 之间所有数的和。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含两个整数 x 和 c。
再接下来 m 行,每行包含两个整数 l 和 r。
输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。
数据范围
-109<=x<=109
1<=n, m<=105
-109<=l <= r<=109
-10000<=c<=10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5
————————————————————————————————————————————————
如果数据范围比较小,就可以用前缀和来做
本题坐标总共有 2*109 个,但是只用到了 n+2*m 个坐标,最多也就是 3 * 105 个坐标。所以通过离散化,把数据范围缩小到 3 * 105 ,就可以用前缀和了。
每个操作都是两个数,所以用一个Pair
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
public class Main {
static int N = 300010;
static int n;
static int m;
static int[] a = new int[N];
static int[] s = new int[N];//前缀和
static ArrayList<Integer> alls = new ArrayList<>();//存储所有需要离散化的下标
//下面两个容器存的都是没有离散化的原始下标
static ArrayList<Pair> add = new ArrayList<>();
static ArrayList<Pair> query = new ArrayList<>();
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader((System.in)));
String[] s1 = br.readLine().split(" ");
n = Integer.parseInt(s1[0]);
m = Integer.parseInt(s1[1]);
for(int i = 0;i < n;i++) {
String[] s2 = br.readLine().split(" ");
int x = Integer.parseInt(s2[0]);
int c = Integer.parseInt(s2[1]);
add.add(new Pair(x, c));
alls.add(x);
}
for(int i = 0;i < m;i++) {
String[] s3 = br.readLine().split(" ");
int l = Integer.parseInt(s3[0]);
int r = Integer.parseInt(s3[1]);
query.add(new Pair(l, r));
alls.add(l);
alls.add(r);
}//alls存储了所有会用到的下标,下一步去重
Collections.sort(alls);
//去重
unique(alls);
//处理插入
for(Pair item : add) {
int x = find(item.x);//找到原数轴上原始下标离散化后的新下标
a[x] += item.y;//新下标位置上加上相应的数
}
//预处理前缀和
for(int i = 1;i <= alls.size();i++) {//处理alls.size()次
s[i] = s[i - 1] + a[i];
}
//处理询问
for(Pair item : query) {
int l = find(item.x);
int r = find(item.y);
System.out.println(s[r] - s[l - 1]);
}
}
private static void unique(ArrayList<Integer> a) {
int j = 0;
for(int i = 0;i < a.size();i++) {
if(i == 0 || a.get(i) != a.get(i - 1)) {
a.set(j++, a.get(i));
}
}//循环结束,0---j-1存的都是不同的元素
}
private static int find(int x) {//返回 x 的下标
int l = 0, r = alls.size() - 1;
while(l < r) {
int mid = l + r >> 1;
if(alls.get(mid) >= x) {
r = mid;
}else {
l = mid + 1;
}
}
//+1表示映射到下标从1开始,因为要用前缀和嘛,看题目要求决定加不加1
return r + 1;
}
}
class Pair{
int x;
int y;
public Pair(int x, int y) {
this.x = x;
this.y = y;
}
}
区间合并
例题:区间合并
给定 n 个区间 [li, ri ],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[1,3] 和 [2, 6] 可以合并成一个区间 [1,6]。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含两个整数 l 和 r 。
输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。
数据范围
1<=n<=100000
-109<=li <= ri<=109
输入样例:
5
1 2
2 4
5 6
7 8
7 9
输出样例:
3
————————————————————————————————————————————————
思路:
1.按区间左端点排序
2.扫描整个区间
上个区间,我们用 st 表示左端点,ed 表示右端点
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Main {
static int n;
static ArrayList<Pair> segs = new ArrayList<>();
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader((System.in)));
n = Integer.parseInt(br.readLine());
for(int i = 0;i < n;i++) {
String[] s1 = br.readLine().split(" ");
int l = Integer.parseInt(s1[0]);
int r = Integer.parseInt(s1[1]);
segs.add(new Pair(l ,r));
}
ArrayList<Pair> res = merge(segs);//不相交区间的集合
System.out.println(res.size());
}
private static ArrayList<Pair> merge(ArrayList<Pair> segs) {
ArrayList<Pair> res = new ArrayList<Pair>();
Collections.sort(segs, new Comparator<Pair>() {
@Override
public int compare(Pair o1, Pair o2) {
return o1.l - o2.l;//左端点升序
}
});
int st = Integer.MIN_VALUE, ed = Integer.MIN_VALUE;
for(Pair cur : segs) {
if(cur.l > ed) {//说明上个区间是个独立的区间
//Integer.MIN_VALUE不是真实的区间端点,所以加个if判断
if(st != Integer.MIN_VALUE) {
res.add(new Pair(st, ed));//每次加的都是上个独立区间
}
//没交集,两个都更新
st = cur.l;
ed = cur.r;
}else {//有交集,更新右端点
ed = Math.max(ed, cur.r);
}
}
//特判一下最后一个区间
if(st != Integer.MIN_VALUE) res.add(new Pair(st, ed));
return res;
}
}
class Pair{
int l;
int r;
public Pair(int l, int r) {
this.l = l;
this.r = r;
}
}