可见山峰对的数量有多少对?
提示:不要怕难题,难题考验人的能力
题目
可见山峰对是这么定义的:
环形山,很多山排在一起,一座山高度为x,顺时针看(或者逆时针看都行),x的邻居直接可见,叫可见山峰对,非邻居的话:可以看见有另一座山高y,而且xy之间,途中任意山峰z,均z<=ming(x,y),则 称xy为可见山峰对 。可以想象一下,反正,我在x山,往那边看(有一条通路),x能看到的山,与x组合起来都叫可见山峰对。
看图:
现在给你一个数组arr,代表山峰的高度,0位置和N-1位置是相邻的环,请问你在这个还型山中,可见山峰对的数量有多少对?
一、审题
示例:比如:
1 3 4 5 2
首先,N=5,邻居相互就有N对=5对,然后非邻居,满足可见山峰对的定义23,35,因,23之间任意山1<=2,35之间任意山1,2<=3。而其他的都不满足这个条件,故,总共有7对。
题意明白了吧。
二、解题
本题的方法要熟悉:
分为2种情况:
一、当arr无重复值,o(1)解决,2N-3个可见山峰对
为啥呢?
看图,用特定的方法推导即可:
数组中最大值为arr,次大值为arr2,他们俩是邻居就算1对了
然后,剩下的N-2个山中,每个x,从x出发,顺时针来遇到某个y,xy之间所有山<=x,y最多找到max,只能有1对;
逆时针也一样,某个z,xz之间所有山<=x,z最多找到max2,只能有1对;
因此,规定max和max2之后,x顺时针,和逆时针就能找到2对,总的2*(N-2)对
加上max和max2邻居,一共2N-4+1=2N-3对。
代码:
public static int mountainPairsNum(int[] arr){
if(arr == null || arr.length < 2) return 0;
int N = arr.length;
if (N == 2) return 1;
if (N == 3) return 3;
return 2 * N - 3;
}
二、当arr有重复值时
比如:3个重复的2,那任意2个2之间都能组成可见山峰对,这3个2与5,与7也都组成了可见山峰对,就比较复杂了
我们需要设计o(n)复杂度的算法
一次遍历数组就能找到任意x左右最大值max和max2来,这里用几个很重要的知识点:单调栈
然后在单调栈弹出的时候清算结果
大流程:
(1)找打arr中的最大值max放栈底,做最大,因此栈stack是由底到顶部,是从大到小的单调栈,用来干嘛?
找max> x <y统计由x组成的山峰对有多少?
(2)从max下一个位置(不论顺时针还是逆时针找),当山x小于max就往栈里面压,保持降序压
(3)遇到新的山y>栈顶高度,这时候就出现了max> x <y,需要将x弹出,结算x的可见山峰对,如果x的个数为k,
如果k==1个,那就是2个山峰对,x见max,x见y
如果k>1个,对外首先是2k(因为k个2,每一个2与max和y都组成了山峰对),对内(222之间)有Ck2组合数个,k个2任选俩有多少组合,他们内部都是可见的山峰对;
(4)当arr遍历到maxindex处,停止,看stack的情况:
——当stack的size>2跟(3)一样,挨个弹出,弹出的时候,y<x<max,也是对外2k个,对内Ck2个
——当stack的size=2时,看下面压了几个max:比如t个,则不管压了t几个,而对内部首先是Ck2对(222之间)
然后看压了t几个
当t=1时,就k对(因为k=1,或k>1)都一个max就k对
当t>1时,俩边max就不一样了,那就是2k对
——当stack的size=1时,就剩下max了,看max的k有几个
k=1时,0对,不成对,Ck2=C12=0
k=2时,1对,相互一对,Ck2=C22=1
k>2时,就是Ck2算组合
由于这里经常用Ck2(k):
代码规整一下:
//从k个相同的元素内部,随意找俩元素组成一个相互可见的山峰对,叫CK2(k),排列组合
public static int CK2(int k){
//组合公式C(n,m)==A(n,m)/A(m,m),现在n==k,m==2
//C(k,2)==A(k,2)/A(2,2)==k(k-1)/2
return k == 1 ? 0 : (k *(k - 1)/2);
}
从maxindex的下一个位置开始寻找:循环坐标方便从N-1找到0位置:
//首先搞定循环坐标
//来到i位置,山总数,就是arr长N=arr.len
//则i的逆时针,下一个坐标是
public static int nextIndex(int i, int N){
return i == N - 1 ? 0 : i + 1;//没到终点,+1,否则就回到0
}
//来到i位置,逆时针上一个坐标是
public static int lastIndex(int i, int N){
return i == 0 ? N - 1 : i - 1;//往顺时针,左走
}
往栈里面放的时候,还要统计山x出现了k>=1次,那放入栈就得是一个对象:
//咋记录次数呢,用栈放一个对象
public static class Record{
public int value;
public int count;
public Record(int v){
value = v;
count = 1;//默认一次,进来就是
}
}
算法大流程,单调栈统计可见山峰对的个数:
public static int mountainPairsNumRepeat(int[] arr){
if (arr == null || arr.length < 2) return 0;
int N = arr.length;
//先找最大值的位置
int maxIndex = 0;
for (int i = 0; i < N; i++) {
maxIndex = arr[i] > arr[maxIndex] ? i : maxIndex;//有更大的更新
}
//压入最大那个
Stack<Record> stack = new Stack<>();
stack.push(new Record(arr[maxIndex]));//最大值作为栈底,永远不出栈的
int i = nextIndex(maxIndex, N);//从max下一个位置开始压栈
int ans = 0;//结果
//只要没到max处,继续压栈
while (i != maxIndex){
//但凡图中遇到相等,count++,小于栈顶,新加,大于栈顶结算
if (arr[i] == stack.peek().value) stack.peek().count++;
else if (arr[i] < stack.peek().value) stack.push(new Record(arr[i]));//新加
while (arr[i] > stack.peek().value){
int k = stack.pop().count;//只需要栈顶的次数,不需要值的
ans += 2 * k + CK2(k);//对外+对内
}
//处理完继续下一个
i = nextIndex(i, N);
}
//栈压完以后,开始清理,分三种情况
//stack.size()>2,==2,<2
while (stack.size() > 2){
int k = stack.pop().count;
ans += 2 * k + CK2(k);//和压栈那个一样,对外+对内都OK
}
if (stack.size() == 2){
int k = stack.pop().count;
//得参考现在这个栈顶了
ans += CK2(k) + stack.peek().count == 1 ? k : 2 * k;//对内肯定不变的,对外不一定,可能只有k,也可能是2k个
}
//自然就是1个最后了
ans += CK2(stack.peek().count);//对外是没有的,对内可能有,不过看情况,1个0对,2个,1对,3个及以上,可能多对,组合
return ans;
}
测试代码:
public static void test(){
int[] arr2 = {5,5,5,5};
int[] arr1 = {1,2,3,4};
System.out.println(mountainPairsNum(arr1));
System.out.println(mountainPairsNumRepeat(arr2));
}
public static void main(String[] args) {
test();
}
总结
提示:重要经验:
1)单调栈解决的问题,寻找x最近的左边/右边,比x大的/小的值,然后弹出x统计两头的信息,这个经常用的。
2)可见山峰对的重复,与不重复的情况,不重复的也可以用重复的解,只不过不重复的时候o(1)一个公式更简单,当然我们要熟悉重复的解法。