第一章(三)

双指针

两大类:
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);
	}
}

位运算

只讲最常用的两种操作

  1. 一个整数 n 的二进制表示中第 k 位是几(最低位是第0位)
    (n >> k) & 1 即可
    右移几位就是取第几位

  2. 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;
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值