青训营 X 码上掘金主题创作-攒青豆题解(n方)

201 阅读6分钟

当青训营遇上码上掘金

题目

现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)

image.png

上图中的例子1:
输入:height=[5,0,2,1,4,0,1,0,3]
输出:17
解释:
5~4是一个区间段(中间的都比它们小),4~3也是一个区间段。
4<54~5中间有三个柱子,4*3-(0+2+1)=9;同理可得下一个区间段可容纳的青豆数为89+8=17。

自己给一个例子2:
输入:height = [0,0,2,1,4,0,1,0,3]
输出:9
解释:(2*1-1)+(3*3-0-1-0)=9

题解

从题意上看,这题更希望我们用区间来解答,不过笔者太菜了就用的暴力解法(只要不是最坏情况复杂化度就不会到n方的)。

题解大概步骤

1.我们用height数组来存放柱子高度,用一个和height同样长度的数组来标记height数组的一些节点(初始化为0)。

int n;//柱子数量
int height[10000];//定义数组
int flag[100000];//这个数组用来标记

cin>>n;//输入n;
//输入数组顺便初始化标记数组
for(int i=0;i<n;i++){
    flag[i]=0;
    cin>>height[i];
}

2.寻找区间的端点(两端点之间的单个值一定比端点值小)并存放在flag数组里面,很明显例子1的端点找出来[5,4,3],找完之后得到的flag一定是[5,0,0,0,4,0,0,0,3]。

3.找到端点之后对原数组进行分区间计算,flag[i]==0的i对应的height里的i一定不是端点。以上面得到的flag=[5,0,0,0,4,0,0,0,3]为例,flag[0]==5>flag[4]=4,flag[0-4]有3个值为0(这里是用来装青豆的,可以参考样例图中的位置),所以4*3=12是这个区间理想的容量,但是还要减去中间三个组织的高度(height[1-3],对应上图分别是0,2,1),所以该区间实际容量为12-0-2-1=9;同理下一个区间的容量为8。

寻找区间端点

1.用一个start来代表起始端点的高度,循环之前初始化默认起始端点为0并标记到flag里面,对应的起始段端点高度就为height[0],例子1里面的起始端点高度就是5了;

int start=height[0];//初始化起点高度start
flag[0]=height[0];//初始化标记,默认0位置是起点

2.每次外循环开始都定义一个Max(用来记录当前节点之后的最大的值,有可能如例子1里面5是起始端点高度且是最大的,那么这之后最大的4一定也是一个端点(下一个起点)),一个index记录下一个起始端点的索引,一个f用来标记是否找到比当前起始高度值大的端点。每次外循环定义初始化是保证内循环用到的都是新的。

for(int i=0;i<n;i++){
    int Max=-1;//最大值初始化
    int index=0;//区间起始索引初始化
    int f=0;//用来标记的
}

3.内循环寻找当前节点之后是否有比当前起始高度(初识化的第一个节点就是起始端点)高或相等的,如果有就记录并更新起始端点索引和起始高度然后退出内循环;如果没有找到,就找最大的那个的值和索引用index记录索引,用Max记录那个值,等到内循环完后进行处理(因为只有内循环完了才能保证那个Max一定是最大的那个)。

for(int j=i+1;j<n;j++){
            if(height[j]!=0){
                //首先我们遍历的这些高度不能为零,因为起始可能为零。
                //如果初始化的第一个起始高度为零,那么下一个起始高度就不应该为零。
                //比如:[0,0,2,1,4,0,1,0,3]的初识起始高度为0,下一个起始高度应该为2
                if(height[j]>=start){
                    //这里找到了比当前起点高或者相等的,也就是下一个起点。
                    start=height[j];//刷新一下起点的值
                    i=j;//更新起始点索引
                    f=1;//这里进行标记找到了比当前起始高度高或者相等的值
                    flag[j]=height[j];//这里用flag标记当前索引是一个端点
                    break;//找到之后跳出循环。
                }
            }
            if(f==0){
                //利用标记判断,这里是当前节点之后的所有节点没有比当前节点高的。
                //这里在后面所有的节点中找最大的那一个(最大的那一个一定是下一个起点)
                //记录那个节点的索引和值。
                if(height[j]>Max){
                    index=j;//记录索引
                    Max=height[j];//记录值
                }
            }
        }

4.当内循环没有找到比当前起始高度高或相等的端点,外循环对当前节点之后的最大的那个节点和对应的值进行处理,也就是更新当前起始点索引并标记这个索引。

if(f==0){
    //这里便是上面循环遍历完之后没找到比当前节点大的,这里进行处理
    i=index;//将当前索引移到下一个起点
    flag[index]=Max;//记录这个索引
}

计算

  • 用例子1里面height1=[5,0,2,1,4,0,1,0,3]找到的区间端点得到的flag一定会为flag1=[5,0,0,0,4,0,0,0,3]。
  • 用例子2里面的height=[0,0,2,1,4,0,1,0,3]经过上面流程得到的flag2=[0,0,2,0,4,0,0,0,3]

1.用sum表示总和(最后输出的那个答案),sum1表示每个区段需要减去的和(也就是每个区段flag[i]==0对应的height[i]的和),pos表示每个区段flag[i]==0出现的次数,statrt1表示一个起始端点高度用来和下一个端点高度做比较(初始值为height[0])。

int sum=0;
int sum1=0;
int pos=0;
int start1=height[0];

2.依次遍历flag,如果flag[i]==0,就pos++计数一次,sum1+=height[i];如果flag[1]!=0,即遍历到了一个起始端点,但分两种情况,情况1 height[0]!=0和情况2 height[0]==0,如果是情况2如例子2,那么我们计算的pos和sum1就要先清零再计算,两情况一个是先计算再清零一个是先清零再计算,唯一不同的是情况2而使用完之后要把height[0]标记为一个不为零的数。

for(int i=0;i<n;i++){
    if(flag[i]==0){
       //代表当前不是端点
       sum1+=height[i];//求该区段要减去的和,如例子1里面5-4之间0,2,1的和
       pos++;//两个端点之间的节点数,如例子1里面的5-4之间有3个节点
    }else{
        if(height[0]!=0){
            //情况1
            int Min=min(start1,flag[i]);//这里进行比对,例子1的话start1就是5了,但我们需要的是小的那个4
            sum+=(Min*pos-sum1);//这里是求每个区段和的总和,
            start1=flag[i];//这里就把start1更新一下,例子1的话下一次比较就是4和3比较了
            //这两个计数值用完了得重新清零。
            pos=0;
            sum1=0;
        }else{
            //情况2,如例2,
            pos=0;
            sum1=0;
            int Min=min(start1,flag[i]);
            sum+=(Min*pos-sum1);
            start1=flag[i];
            height[0]=1;
        }
    }
}

完整代码

int n;//柱子数量
int height[10000];//定义数组
int flag[100000];//这个数组用来标记

cin>>n;//输入n;
//输入数组顺便初始化标记数组
for(int i=0;i<n;i++){
    flag[i]=0;
    cin>>height[i];
} 

int start=height[0];//初始化起点高度start
flag[0]=height[0];//初始化标记,默认0位置是起点

for(int i=0;i<n;i++){
    int Max=-1;//最大值初始化
    int index=0;//区间起始索引初始化
    int f=0;//用来标记的
    
    for(int j=i+1;j<n;j++){
            if(height[j]!=0){
                //首先我们遍历的这些高度不能为零,因为起始可能为零。
                //如果初始化的第一个起始高度为零,那么下一个起始高度就不应该为零。
                //比如:[0,0,2,1,4,0,1,0,3]的初识起始高度为0,下一个起始高度应该为2
                if(height[j]>=start){
                    //这里找到了比当前起点高或者相等的,也就是下一个起点。
                    start=height[j];//刷新一下起点的值
                    i=j;//更新起始点索引
                    f=1;//这里进行标记找到了比当前起始高度高或者相等的值
                    flag[j]=height[j];//这里用flag标记当前索引是一个端点
                    break;//找到之后跳出循环。
                }
            }
            if(f==0){
                //利用标记判断,这里是当前节点之后的所有节点没有比当前节点高的。
                //这里在后面所有的节点中找最大的那一个(最大的那一个一定是下一个起点)
                //记录那个节点的索引和值。
                if(height[j]>Max){
                    index=j;//记录索引
                    Max=height[j];//记录值
                }
            }
        }
        
      if(f==0){
    //这里便是上面循环遍历完之后没找到比当前节点大的,这里进行处理
    i=index;//将当前索引移到下一个起点
    flag[index]=Max;//记录这个索引
    }  
}

int sum=0;
int sum1=0;
int pos=0;
int start1=height[0];

for(int i=0;i<n;i++){
    if(flag[i]==0){
       //代表当前不是端点
       sum1+=height[i];//求该区段要减去的和,如例子1里面5-4之间0,2,1的和
       pos++;//两个端点之间的节点数,如例子1里面的5-4之间有3个节点
    }else{
        if(height[0]!=0){
            //情况1
            int Min=min(start1,flag[i]);//这里进行比对,例子1的话start1就是5了,但我们需要的是小的那个4
            sum+=(Min*pos-sum1);//这里是求每个区段和的总和,
            start1=flag[i];//这里就把start1更新一下,例子1的话下一次比较就是4和3比较了
            //这两个计数值用完了得重新清零。
            pos=0;
            sum1=0;
        }else{
            //情况2,如例2,
            pos=0;
            sum1=0;
            int Min=min(start1,flag[i]);
            sum+=(Min*pos-sum1);
            start1=flag[i];
            height[0]=1;
        }
    }
}
cout<<sum<<endl;

小结

笔者功底有限没能想出更好的解法,希望能够帮助到大家,如果有伙伴有更好的方法也可以分享给我。