最大线段重合问题
给定很多线段,每个线段都有两个数[start, end], 表示线段开始位置和结束位置,左右都是闭区间 规定:
1)线段的开始和结束位置一定都是整数值
2)线段重合区域的长度必须>=1 返回线段最多重合区域中,包含了几条线段
思路:
此处有两种实现方式:
方式一:
1、找到线段数组中的最小值和最大值,然后再从最小值每次加0.5,然后找所有线段中开始小于当前数加0.5的,并且结束大于当前数加0.5的,找到重合次数的最大值。
方式2:
维护一个小根堆,首先将线段数组中的开始节点从小到大排序,首先将第一个线段的结尾数字放入这个小根堆里面,然后用第二个数的开始节点进入小根堆里面去判断,如果第一个线段的结尾小于第二个线段的开始,那么弹出第一个线段的结尾(此时没有交点,且因为线段数组的开始节点都是排序了的,对于后面的线段也没有焦点),如果第一个线段的结尾大于第二个线段的开始,那么此时则是有焦点的,将第二个线段的结尾放入小根堆里面,这样依次的遍历下去,最终得到一个最大的重合结果。
public static int maxCover1(int[][] lines) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i = 0; i < lines.length; i++) {
min = Math.min(min, lines[i][0]);
max = Math.max(max, lines[i][1]);
}
int cover = 0;
for (double p = min + 0.5; p < max; p += 1) {
int cur = 0;
for (int i = 0; i < lines.length; i++) {
if (lines[i][0] < p && lines[i][1] > p) {
cur++;
}
}
cover = Math.max(cover, cur);
}
return cover;
}
public static int maxCover2(int[][] m) {
Line[] lines = new Line[m.length];
for (int i = 0; i < m.length; i++) {
lines[i] = new Line(m[i][0], m[i][1]);
}
Arrays.sort(lines, new StartComparator());
// 小根堆,每一条线段的结尾数值,使用默认的
PriorityQueue<Integer> heap = new PriorityQueue<>();
int max = 0;
for (int i = 0; i < lines.length; i++) {
// lines[i] -> cur 在黑盒中,把<=cur.start 东西都弹出
while (!heap.isEmpty() && heap.peek() <= lines[i].start) {
heap.poll();
}
heap.add(lines[i].end);
max = Math.max(max, heap.size());
}
return max;
}
public static class Line {
public int start;
public int end;
public Line(int s, int e) {
start = s;
end = e;
}
}
public static class StartComparator implements Comparator<Line> {
@Override
public int compare(Line o1, Line o2) {
return o1.start - o2.start;
}
}
方法2时间复杂度估算:首先所有线段都要遍历一遍,小根堆最坏情况是所有线段的结尾位置最多进
一次小根堆,最多出一次小根堆,一共有N个结尾。小根堆调整代价为logNm,整体的复杂度为
O(N*logN)
手写堆
系统提供的堆无法做到的事情:
1)已经入堆的元素,如果参与排序的指标方法变化,系统提供的堆无法做到时间复杂度O(logN)调整!都是O(N)的调整!
2)系统提供的堆只能弹出堆顶,做不到自由删除任何一个堆中的元素,或者说,无法在时间复杂度O(logN)内完成!
一定会高于O(logN)【要先找到被改变的那个元素,然后再做调整】
根本原因:无反向索引表
系统为何不提供反向索引表:java–偏向应用(往往是给一个弹出一个,增加了反倒浪费内存空间)
c++ 委员会有提供。
思路:
1)建立反向索引表
2)建立比较器
3)核心在于各种结构相互配合,非常容易出错
/*
* T一定要是非基础类型,有基础类型需求包一层
*/
public class HeapGreater<T> {
private ArrayList<T> heap;
//反向索引表,表示对象存在数组中哪个位置
private HashMap<T, Integer> indexMap;
//堆的大小
private int heapSize;
//自定义的比较器
private Comparator<? super T> comp;
public HeapGreater(Comparator<? super T> c) {
heap = new ArrayList<>();
indexMap = new HashMap<>();
heapSize = 0;
comp = c;
}
public boolean isEmpty() {
return heapSize == 0;
}
public int size() {
return heapSize;
}
//此处通过反向索引表直接O(1)的复杂度查询
public boolean contains(T obj) {
return indexMap.containsKey(obj);
}
public T peek() {
return heap.get(0);
}
public void push(T obj) {
//在堆里加在最后的位置
heap.add(obj);
//反向索引表里面加上当前元素
indexMap.put(obj, heapSize);
//通过heapInsert调整heap以及indexMap
heapInsert(heapSize++);
}
//弹出堆顶元素
public T pop() {
//获取堆顶元素
T ans = heap.get(0);
//堆顶元素和最后一个元素交换
swap(0, heapSize - 1);
//反向索引表移除这个堆顶元素
indexMap.remove(ans);
//将最后一个元素移除
heap.remove(--heapSize);
//从0位置往下调整
heapify(0);
return ans;
}
//高效删除某一个对象,不同于普通堆只能删除堆顶元素
public void remove(T obj) {
//获取最后一个元素用来替代要删除对象的位置
T replace = heap.get(heapSize - 1);
//获取要删除对象的index
int index = indexMap.get(obj);
//先从反向索引表里面删除当前要删除的对象
indexMap.remove(obj);
//堆移除最后一个元素
heap.remove(--heapSize);
//如果移除的元素不是要替换的元素
if (obj != replace) {
//将删除元素的位置设置成要替代的元素
heap.set(index, replace);
//将替换的元素及其index放入反向索引表中
indexMap.put(replace, index);
//重新向上或向下调整堆
resign(replace);
}
}
public void resign(T obj) {
heapInsert(indexMap.get(obj));
heapify(indexMap.get(obj));
}
// 请返回堆上的所有元素
public List<T> getAllElements() {
List<T> ans = new ArrayList<>();
for (T c : heap) {
ans.add(c);
}
return ans;
}
private void heapInsert(int index) {
//通过比较器向上比较
while (comp.compare(heap.get(index), heap.get((index - 1) / 2)) < 0) {
swap(index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapify(int index) {
int left = index * 2 + 1;
while (left < heapSize) {
int best = left + 1 < heapSize && comp.compare(heap.get(left + 1), heap.get(left)) < 0 ? (left + 1) : left;
best = comp.compare(heap.get(best), heap.get(index)) < 0 ? best : index;
if (best == index) {
break;
}
swap(best, index);
index = best;
left = index * 2 + 1;
}
}
private void swap(int i, int j) {
//堆和反向索引表同步换位置
T o1 = heap.get(i);
T o2 = heap.get(j);
heap.set(i, o2);
heap.set(j, o1);
indexMap.put(o2, i);
indexMap.put(o1, j);
}
}
手动改写堆题目练习
给定一个整型数组,int[] arr;和一个布尔类型数组,boolean[] op 两个数组一定等长,假设长度为N,arr[i]表示客户编号,op[i]表示客户操作 arr = [ 3 , 3 , 1 , 2, 1, 2, 5… op = [ T , T, T, T, F, T, F… 依次表示:3用户购买了一件商品,3用户购买了一件商品,1用户购买了一件商品,2用户购买了一件商品,1用户退货了一件商品,2用户购买了一件商品,5用户退货了一件商品…
一对arr[i]和op[i]就代表一个事件: 用户号为arr[i],op[i] == T就代表这个用户购买了一件商品 op[i] == F就代表这个用户退货了一件商品 现在你作为电商平台负责人,你想在每一个事件到来的时候, 都给购买次数最多的前K名用户颁奖。 所以每个事件发生后,你都需要一个得奖名单(得奖区)。
得奖系统的规则:
1,如果某个用户购买商品数为0,但是又发生了退货事件,则认为该事件无效,得奖名单和上一个事件发生后一致,例子中的5用户
2,某用户发生购买商品事件,购买商品数+1,发生退货事件,购买商品数-1
3,每次都是最多K个用户得奖,K也为传入的参数如果根据全部规则,得奖人数确实不够K个,那就以不够的情况输出结果
4,得奖系统分为得奖区和候选区,任何用户只要购买数>0,一定在这两个区域中的一个
5,购买数最大的前K名用户进入得奖区,在最初时如果得奖区没有到达K个用户,那么新来的用户直接进入得奖区
6,如果购买数不足以进入得奖区的用户,进入候选区
7,如果候选区购买数最多的用户,已经足以进入得奖区,该用户就会替换得奖区中购买数最少的用户(大于才能替换),如果得奖区中购买数最少的用户有多个,就替换最早进入得奖区的用户如果候选区中购买数最多的用户有多个,机会会给最早进入候选区的用户
8,候选区和得奖区是两套时间,因用户只会在其中一个区域,所以只会有一个区域的时间,另一个没有从得奖区出来进入候选区的用户,得奖区时间删除,进入候选区的时间就是当前事件的时间(可以理解为arr[i]和op[i]中的i)从候选区出来进入得奖区的用户,候选区时间删除,进入得奖区的时间就是当前事件的时间(可以理解为arr[i]和op[i]中的i)
9,如果某用户购买数==0,不管在哪个区域都离开,区域时间删除,离开是指彻底离开,哪个区域也不会找到该用户如果下次该用户又发生购买行为,产生>0的购买数,会再次根据之前规则回到某个区域中,进入区域的时间重记
请遍历arr数组和op数组,遍历每一步输出一个得奖名单 public List<List> topK (int[] arr, boolean[] op, int k)
代码如下:
public static class Customer {
public int id;
//买了几件商品
public int buy;
//进入得奖区域的时间或者进入候选区域的时间
public int enterTime;
public Customer(int v, int b, int o) {
id = v;
buy = b;
enterTime = 0;
}
}
//将买的多的且进入时间最早的放前面
public static class CandidateComparator implements Comparator<Customer> {
@Override
public int compare(Customer o1, Customer o2) {
return o1.buy != o2.buy ? (o2.buy - o1.buy) : (o1.enterTime - o2.enterTime);
}
}
//将买的最少的且进入时间最早的放前面(扔掉)
public static class DaddyComparator implements Comparator<Customer> {
@Override
public int compare(Customer o1, Customer o2) {
return o1.buy != o2.buy ? (o1.buy - o2.buy) : (o1.enterTime - o2.enterTime);
}
}
public static class WhosYourDaddy {
//哪个id对应哪个customer
private HashMap<Integer, Customer> customers;
//使用之前的加强堆
//定义候选区以及得奖区都放入堆中
private HeapGreater<Customer> candHeap;
private HeapGreater<Customer> daddyHeap;
//限制得奖区的大小
private final int daddyLimit;
public WhosYourDaddy(int limit) {
customers = new HashMap<>();
//根据自定义的比较器来定义自己的加强堆
candHeap = new HeapGreater<>(new CandidateComparator());
daddyHeap = new HeapGreater<>(new DaddyComparator());
daddyLimit = limit;
}
// 当前处理i号事件,arr[i] -> id, buyOrRefund
public void operate(int time, int id, boolean buyOrRefund) {
//如果退货并且不包含此用户,则返回
if (!buyOrRefund && !customers.containsKey(id)) {
return;
}
if (!customers.containsKey(id)) {
customers.put(id, new Customer(id, 0, 0));
}
Customer c = customers.get(id);
if (buyOrRefund) {
c.buy++;
} else {
c.buy--;
}
if (c.buy == 0) {
customers.remove(id);
}
if (!candHeap.contains(c) && !daddyHeap.contains(c)) {
if (daddyHeap.size() < daddyLimit) {
c.enterTime = time;
daddyHeap.push(c);
} else {
c.enterTime = time;
candHeap.push(c);
}
} else if (candHeap.contains(c)) {
//在候选区里面如果购买数量现在为0则删除
if (c.buy == 0) {
candHeap.remove(c);
} else {
//不为0则调整
candHeap.resign(c);
}
} else {
//在得奖区
if (c.buy == 0) {
daddyHeap.remove(c);
} else {
daddyHeap.resign(c);
}
}
daddyMove(time);
}
public List<Integer> getDaddies() {
//返回堆上所有元素
List<Customer> customers = daddyHeap.getAllElements();
List<Integer> ans = new ArrayList<>();
for (Customer c : customers) {
ans.add(c.id);
}
return ans;
}
private void daddyMove(int time) {
if (candHeap.isEmpty()) {
return;
}
if (daddyHeap.size() < daddyLimit) {
Customer p = candHeap.pop();
//重置时间点
p.enterTime = time;
daddyHeap.push(p);
} else {
if (candHeap.peek().buy > daddyHeap.peek().buy) {
Customer oldDaddy = daddyHeap.pop();
Customer newDaddy = candHeap.pop();
oldDaddy.enterTime = time;
newDaddy.enterTime = time;
daddyHeap.push(newDaddy);
candHeap.push(oldDaddy);
}
}
}
}
public static List<List<Integer>> topK(int[] arr, boolean[] op, int k) {
List<List<Integer>> ans = new ArrayList<>();
//建好一个结构whosyourdady并初始化大小
WhosYourDaddy whoDaddies = new WhosYourDaddy(k);
for (int i = 0; i < arr.length; i++) {
//每次操作放入此结构中进行
whoDaddies.operate(i, arr[i], op[i]);
//完成后再返回就行了
ans.add(whoDaddies.getDaddies());
}
return ans;
}
// 干完所有的事,模拟,不优化
public static List<List<Integer>> compare(int[] arr, boolean[] op, int k) {
//id对应的用户
HashMap<Integer, Customer> map = new HashMap<>();
//候选区
ArrayList<Customer> cands = new ArrayList<>();
//得奖区
ArrayList<Customer> daddy = new ArrayList<>();
//每一步的得奖区
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
//当前用户的id
int id = arr[i];
//当前用户是买了还是退货了呢由op决定
boolean buyOrRefund = op[i];
//如果此时是个退货行为而且没这个用户
if (!buyOrRefund && !map.containsKey(id)) {
//等同于这个事件没有发生,和上一次事件保持一直
ans.add(getCurAns(daddy));
continue;
}
// 没有发生用户购买数为0并且又退货了的整体事件
// 用户之前购买数是0,此时买货事件
// 用户之前购买数>0, 此时买货
// 用户之前购买数>0, 此时退货
if (!map.containsKey(id)) {
//如果map里面没有包含这个用户,此时将当前用户加入进去,后续流程调整
map.put(id, new Customer(id, 0, 0));
}
// 买、卖
Customer c = map.get(id);
if (buyOrRefund) {
c.buy++;
} else {
c.buy--;
}
if (c.buy == 0) {
map.remove(id);
}
// c
// 下面做 候选区和得奖区也不能有buy=0的
if (!cands.contains(c) && !daddy.contains(c)) {
if (daddy.size() < k) {
//如果得奖区的数量小于k则将当前用户加入得奖区,并设置当前时间
c.enterTime = i;
daddy.add(c);
} else {
//如果得奖区满了则将当前用户放入候选区
c.enterTime = i;
cands.add(c);
}
}
//清除购买数量为0的候选区
cleanZeroBuy(cands);
//清除购买数为0的得奖区
cleanZeroBuy(daddy);
//根据排序标准排序候选区
cands.sort(new CandidateComparator());
//根据排序标准排序得奖区
daddy.sort(new DaddyComparator());
move(cands, daddy, k, i);
ans.add(getCurAns(daddy));
}
return ans;
}
public static void move(ArrayList<Customer> cands, ArrayList<Customer> daddy, int k, int time) {
//候选区空的
if (cands.isEmpty()) {
return;
}
// 候选区不为空
//得奖区的size<k
if (daddy.size() < k) {
//直接将当前数放入得奖区,一定是得奖区某个用户当前退货后购买数为0
//然后得奖区移除购买数为0的用户,放入候选区第一个用户进去的情况
Customer c = cands.get(0);
//更新进入时间为当前时间
c.enterTime = time;
daddy.add(c);
cands.remove(0);
} else { // 得奖区满了,候选区有东西
//候选区购买量最大的大于了得奖区最小的则做替换,同一个时间替换一次
if (cands.get(0).buy > daddy.get(0).buy) {
//得奖区第一个取出来
Customer oldDaddy = daddy.get(0);
//再从得奖区移除这个人
daddy.remove(0);
//将候选区的第一个加入到得奖区,并将这个人从候选区中移除
Customer newDaddy = cands.get(0);
cands.remove(0);
//调整时间为当前时间
newDaddy.enterTime = time;
oldDaddy.enterTime = time;
//最后加入到得奖区和候选区
daddy.add(newDaddy);
cands.add(oldDaddy);
}
}
}
public static void cleanZeroBuy(ArrayList<Customer> arr) {
List<Customer> noZero = new ArrayList<Customer>();
for (Customer c : arr) {
if (c.buy != 0) {
noZero.add(c);
}
}
arr.clear();
for (Customer c : noZero) {
arr.add(c);
}
}
public static List<Integer> getCurAns(ArrayList<Customer> daddy) {
List<Integer> ans = new ArrayList<>();
for (Customer c : daddy) {
ans.add(c.id);
}
return ans;
}
*时间复杂度估算:
首先得奖区有K个
所有的事件进得奖区复杂度为O(NlogK)
候选区最多有N个元素 O(NlogK)
每个事件到来的时候都要调整一个长度为K的链
所以整个时间复杂度估算为N(logN + logK + K)
**