---------------刷算法题的乐趣就是在刷题过程中,学习并且掌握了新知识,巩固了旧知识,这很nice!!! 就是一个小的知识点关查资料就要十个网页窗口,除了有点费脑子之外就是怕红红的ce----------------
前言
y1s1,java语言有点太过正经了,,,
一、离散化算法
1.概念
简单来说:有一个数组,下标有:1,400,300000, 100000000 …。 但是我们总不可能把整个数组表示出来,又因为数组其中有些是没有储存元素 的,所以我们可以利用离散化算法,把所有的没有元素的数组空间去掉。而且如果当题目是数轴 有负数 也会感到无从下手。
离散化的本质,是映射,将间隔很大的点,映射到相邻的数组元素中。减少对空间的需求,也减少计算量。
2.做题思想
- 用一个数组来装下标
- 对装下标的数组进行排序,去重
- 开一个Pair类的数组 k(first)装下标 , v(second)装原本所在下标的值。
- 根据题目解题( 构造一个函数(二分查找) 用来找离散后的下标) find函数是重点!!
3.解题模板
借用yxc大佬的模板(C++版本二)
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到1, 2, ...n
}
模板看不懂没关系,通过题目理解!!!
上菜上菜!! :::
4.例题
题目描述
假定有一个无限长的数轴,数轴上每个坐标上的数都是0。
现在,我们首先进行 n 次操作,每次操作将某一位置x上的数加c。
近下来,进行 m 次询问,每个询问包含两个整数l和r,你需要求出在区间[l, r]之间的所有数的和。
输入格式
第一行包含两个整数n和m。
接下来 n 行,每行包含两个整数x和c。
再接下里 m 行,每行包含两个整数l和r。
输出格式
共m行,每行输出一个询问中所求的区间内数字和。
数据范围
109≤x≤109,
≤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
答案:
import java.util.*;
import javafx.util.*;
public class Test01 {
//300010是因为(n次操作 + m次询问)的最大值是3*100000, +10是怕数组出界
static int N = 300010;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
//用来存储 数组下标 的数组
List<Integer> alls = new ArrayList<>();
//根据题意 需要一个数组来储存 下标 和 所加的值
List<Pair<Integer, Integer>> adds = new ArrayList<>();
// 这个数组用来储存 区间
List<Pair<Integer, Integer>> query = new ArrayList<>();
int[] a = new int[N];
int[] s = new int[N];
//根据题意进行 n 次操作,每次操作将某一位置x上的数加c
while (n -- > 0) {
int x = sc.nextInt();
int c = sc.nextInt();
adds.add(new Pair<>(x, c));
//将需要离散化的 x(下标) 储存到数组
alls.add(x);
}
//根据题意进行 m 次询问,每个询问包含两个整数l和r,分别为每个区间的左右端点
while (m -- > 0) {
int l = sc.nextInt();
int r = sc.nextInt();
query.add(new Pair<>(l, r));
//将需要离散化的 l,r(下标) 储存到数组
alls.add(l);
alls.add(r);
}
//将数组进行排序
Collections.sort(alls);
// unique是把 存下标的数组 里面重复的下标删去
// list.subList(a, b) 这个方法是数组从[0, unique - 1]截取出来的子数组
//所以unique的返回值 不需要 减一
alls = alls.subList(0, unique(alls));
// *****重点*****
for (Pair<Integer, Integer> item : adds) {
//注意这里不是返回原来的数组存储的那个下标!!!
//请把目光看向 构造的 find 方法
int index = find(alls, item.getKey());
a[index] = item.getValue();
}
//前缀和计算
for (int i = 1; i <= alls.size(); i ++)
s[i] = s[i - 1] + a[i];
//用find方法把 原来存储区间左右端点的值 转化为 离散化排好序的数组里面找对应的下标
for (Pair<Integer, Integer> item : query) {
int l = find(alls, item.getKey());
int r = find(alls, item.getValue());
System.out.println(s[r] - s[l - 1]);
}
}
public static int find(List<Integer> list, int x) {
int l = 0;
//这里的右边界是 给某个位置加值 的数组的大小
//就是 我们离散化的数组
int r = list.size() - 1;
//二分查找模板
while (r > l) {
int mid = l + r >> 1;
if (x <= list.get(mid))
r = mid;
else
l = mid + 1;
}
//返回值加一是因为待会要进行 前缀和计算 ,为了处理边界问题!
return l + 1;
}
//因为Java没有unique方法 我们需要自己写
public static int unique(List<Integer> list) {
int index = 0;
//当这个数是第一个数,或者这个数与它的前一个数不相等时,他才是唯一的数
for (int i = 0; i < list.size(); i ++) {
if (i == 0 || list.get(i) != list.get(i - 1))
list.set(index ++, list.get(i));
}
return index;
}
}
我尽力了 。。。。
如果前缀和不懂的,可以看一下我的上一篇博客:前缀和与差分思想 哈哈哈!
有什么不懂请到评论区讨论吧,语文基础比较差有什么地方说错请帮忙指出来,谢谢你!
二、区间合并
1.概述
Emmmmm,就是将包含的 或者 相交的区间 合并。
2.解题步骤
- 先将所有区间进行从左到右排序(使区间之间只有剩下三种情况:相等 相交 相离)
- 进行合并
3.解题模板
yxc大佬 yyds!!!
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
vector<PII> res;
//因为C++中sort函数有对PII的有效:先判定first, 如果first相同,则判断second
sort(segs.begin(), segs.end());
//先定义一个区间
int st = -2e9, ed = -2e9;
for (auto seg : segs)
//当 该区间的右端点 < 下一个区间的左端点(无交集)
if (ed < seg.first)
{
//判定是不是原来定义的那个区间,不是就加到数组中
if (st != -2e9) res.push_back({st, ed});
//重新定义
st = seg.first, ed = seg.second;
}
//相交 或者 相等 判断右端点,谁大右端点就是谁!
else ed = max(ed, seg.second);
//将最后的区间加到数组中如果当初就没有区间,
if (st != -2e9) res.push_back({st, ed});
//更新数组
segs = res;
}
说实话,学习算法思想还是题目管用,上菜!!!::::
4.例题
4.1、题目描述
给定 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
import java.util.*;
import javafx.util.*;
public class Test01 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
List<Pair<Integer, Integer>> list = new ArrayList<>();
while (n -- > 0) {
int l = sc.nextInt();
int r = sc.nextInt();
list.add(new Pair<>(l, r));
}
list = merge(list);
System.out.println(list.size());
}
public static List<Pair<Integer, Integer>> merge(List<Pair<Integer, Integer>> segs) {
//我采用匿名内部类构造一个比较器,用来比较Pair这个类
Collections.sort(segs, new Comparator<Pair<Integer, Integer>>(){
@Override
public int compare(Pair<Integer, Integer> p1, Pair<Integer, Integer> p2) {
//返回1表示顺序不变,-1表示顺序相反
if (p1.getKey() > p2.getKey()) return 1;
else if (p1.getKey() < p2.getKey()) return -1;
else {
if (p1.getValue() >= p2.getValue()) return 1;
else return -1;
}
}
});
Integer st = Integer.MIN_VALUE;
Integer ed = Integer.MIN_VALUE;
List<Pair<Integer, Integer>> res = new ArrayList<>();
//直接套用模板
for (Pair<Integer, Integer> seg : segs) {
if (ed < seg.getKey()) {
if (st != Integer.MIN_VALUE)
res.add(new Pair<>(st, ed));
st = seg.getKey();
ed = seg.getValue();
}
else
ed = max(ed, seg.getValue());
}
if (st != Integer.MIN_VALUE)
res.add(new Pair<>(st, ed));
return res;
}
public static Integer max(Integer a, Integer b) {
if (a >= b)
return a;
else
return b;
}
}
是不是感到无压力? 趁热打铁来一道19年的美团笔试题吧!!!
上菜!!!!::::
在 acwing.com 这个网站的第759道题
4.2、题目描述
在二维平面上有一个无限的网格图形,初始状态下,所有的格子都是空白的。
现在有 n 个操作,每个操作是选择一行或一列,并在这行或这列上选择两个端点网格,把以这两个网格为端点的区间内的所有网格染黑(包含这两个端点)。
问经过 n 次操作以后,共有多少个格子被染黑,重复染色同一格子只计数一次。
输入格式
第一行包含一个正整数 n。
接下来 n 行,每行包含四个整数 x1,y1,x2,y2,表示一个操作的两端格子坐标。
若 x1=x2 则是在一列上染色,若 y1=y2 则是在一行上染色。
保证每次操作是在一行或一列上染色。
输出格式
包含一个正整数,表示被染黑的格子的数量。
数据范围
1≤n≤10000,
−109≤x1,y1,x2,y2≤109
输入样例:
3
1 2 3 2
2 5 2 3
1 4 3 4
输出样例:
8
因为是二维的,所以要思考的有需要怎么把区间装进数组,该怎么装方便些,怎么进行查重!
想不出的直接看到代码的末尾!
import java.util.*;
public class test02 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
List<seg> rows = new ArrayList<>();
List<seg> cols = new ArrayList<>();
while (n -- > 0) {
int x1 = sc.nextInt();
int y1 = sc.nextInt();
int x2 = sc.nextInt();
int y2 = sc.nextInt();
if (x1 == x2)
cols.add(new seg(x1, Math.min(y1, y2), Math.max(y1, y2)));
else
rows.add(new seg(y1, Math.min(x1, x2), Math.max(x1, x2)));
}
long num = 0;
num = merge(rows) + merge(cols);
for (seg row : rows)
for (seg col : cols)
//查重
if (row.k >= col.sml && row.k <= col.big && col.k >= row.sml && col.k <= row.big) {-- num ;}
System.out.println(num);
}
public static long merge(List<seg> list) {
long num = 0;
List<seg> res = new ArrayList<>();
list.sort((s1, s2) -> {
if (s1.k != s2.k)
return s1.k - s2.k;
else if (s1.sml != s2.sml)
return s1.sml - s2.sml;
else
return s1.big - s2.big;
});
int st = Integer.MIN_VALUE;
int ed = Integer.MIN_VALUE;
int k = list.get(0).k;
for (seg item : list) {
if (k == item.k) {
if (ed < item.sml) {
if (st != Integer.MIN_VALUE) {
res.add(new seg(k, st, ed));
//求出染的格子个数
num += ed - st + 1;
}
st = item.sml;
ed = item.big;
}
else
ed = Math.max(ed, item.big);
} else {
if (st != Integer.MIN_VALUE) {
res.add(new seg(k, st, ed));
num += ed - st + 1;
}
k = item.k;
st = item.sml;
ed = item.big;
}
}
if (st != Integer.MIN_VALUE) {
res.add(new seg(k, st, ed));
num += ed - st + 1;
}
list.clear();
list.addAll(res);// 等同于 list = res
return num;
}
static class seg{
int k;
int sml;
int big;
public seg(int k, int sml, int big){
this.k = k;
this.sml = sml;
this.big = big;
}
}
}
我们需要构造一个类,来存相同的行(列)和 当前行(列)的区间
查重就是 当一个数符合 : 在相同行的情况下, 这个相同行 大于 列的区间最小值, 小于 列的区间最大值
在相同列的情况下,这个相同列 大于行的区间最小值, 小于 行的区间最大值!
说到底就是 在有效的行列区间重合的部分。 看图
谢谢您的阅读
自我总结
两天三道题 累了。。。。
说真的,Java语言太正经了。。
通过这三道题,我学会了:
- 知道Pair这个类,并会使用
- 使用匿名内部类和对比较器的使用(从一开始不知道怎么排数组-> Collections.sort -> list.sort)越来越灵活了!!!
- 认识到Integer等的常量 Integer.MIN_VALUE=>最小值 (复习了Integer 从-128 ~ 127是储存在堆中的常量池中)
- 学会 离散化算法 和 区间合并 !