【问题标题】:Add water between in a bar chart在条形图之间加水
【发布时间】:2017-04-13 17:58:17
【问题描述】:

最近在 glassdoor-like 网站遇到一个面试问题,我找不到解决这个问题的优化解决方案:

这与困水问题完全不同。请通读示例。

给定一个输入数组,每个元素代表塔的高度,将浇水的量,索引号表示浇水的位置。每个塔的宽度为1。浇水后打印图形。

注意事项:

  1. *表示塔,w表示1水量。

  2. 浇注位置永远不会在峰值位置。无需考虑分水情况。

    (如果您为这种情况提供了解决方案,则可以加分,您可以假设如果在峰值位置浇注 N/2 水,则 N/2 水向左,N/2 水向右。)

峰的定义:峰位的高度大于其旁边的左右索引。)

  1. 假设有 2 个极高的墙靠近直方图。
    所以如果水量超过直方图的容量,
    您应该指出容量编号并继续前进。请参见示例 2。

  2. 假设水先向左,参见示例 1

示例 1:

int[] heights = {4,2,1,2,3,2,1,0,4,2,1}
It look like:

*       *
*   *   **
** ***  **
******* ***
+++++++++++  <- there'll always be a base layer
42123210431
Assume given this heights array, water amout 3, position 2:

打印:

*       *
*ww *   **
**w***  **
******* ***
+++++++++++ 

示例 2:

int[] heights = {4,2,1,2,3,2,1,0,4,2,1}, water amout 32, position 2

打印:

capacity:21

wwwwwwwwwww
*wwwwwww*ww
*www*www**w
**w***ww**w
*******w***
+++++++++++ 

起初我以为它就像the trapping water problem,但我错了。有人有解决这个问题的算法吗?

欢迎在代码中提供解释或 cmets。

注意:

捕水问题问的是容量,但这个问题引入了两个变量:水量和浇注指数。此外,水有流动的偏好。所以它不像困水问题。

【问题讨论】:

  • 如果浇水位置是 3 个,您希望水流向左、向右(最深的山谷和最大容量)还是随机的?
  • @m69 先离开。如果左侧已满/无法加水,请向右走。
  • 那么看起来很简单。制作一个 2D 网格,从顶部开始每一滴,往下走,如果不可能向左,如果不可能向右,重复直到卡住,重复每一滴。 (想一想,你可能必须直接穿过最顶层的水滴,看看那层是否还有空间,如果没有,则回溯。嗯,这毕竟不是那么简单:-) )
  • @m69。是的。毕竟没那么简单。理论上,会有一个线性时间解。与那个著名的陷雨问题相比,这种情况需要打印全图,而且很难,因为有两种情况:容量不足和容量过剩。
  • @Ken White。你还认为这是一个家庭作业的问题吗?还是重复的困雨问题?

标签: algorithm


【解决方案1】:

我找到了这个问题的 Python 解决方案。但是,我不熟悉 Python,所以我在这里引用代码。希望有人知道 Python 可以提供帮助。

@z026 编码

def pour_water(terrains, location, water):
print 'location', location 
print 'len terrains', len(terrains)
waters = [0] * len(terrains)
while water > 0: 
    left = location - 1
    while left >= 0:
        if terrains[left] + waters[left] > terrains[left + 1] + waters[left + 1]:
            break
        left -= 1

    if terrains[left + 1] + waters[left + 1] < terrains[location] + waters[location]:
        location_to_pour = left + 1
        print 'set by left', location_to_pour 
    else: 
        right = location + 1
        while right < len(terrains):
            if terrains[right] + waters[right] > terrains[right - 1] + waters[right - 1]:
                print 'break, right: {}, right - 1:{}'.format(right, right - 1) 
                break
            right += 1 

        if terrains[right - 1] + waters[right - 1] < terrains[location] + waters[right - 1]:
            location_to_pour = right - 1
            print 'set by right', location_to_pour
        else:
            location_to_pour = location 
            print 'set to location', location_to_pour

    waters[location_to_pour] += 1 

    print location_to_pour
    water -= 1

max_height = max(terrains)

for height in xrange(max_height, -1, -1):
    for i in xrange(len(terrains)):
        if terrains + waters < height:
            print ' ',
        elif terrains < height <= terrains + waters:
            print 'w',
        else:
            print '+',
    print ''

【讨论】:

  • 这和我的代码差不多,除了它们将水的高度和地形的高度分开,我把它们累积起来,这样我就可以去掉很多添加了。如果你从我的水高中减去地形高度,那么你就会得到这里的水高。
  • @maraca 是的。我试过你的代码。完美解决。
【解决方案2】:

由于无论如何您都必须生成并打印出数组,我可能会选择一种递归方法来保持O(rows*columns) 的复杂性。请注意,每个单元格最多可以“访问”两次。

在高层次上:首先向下递归,然后向左,然后向右,然后填充当前单元格。

但是,这遇到了一个小问题:(假设这是一个问题)

*w    *                *     *
**ww* *   instead of   **ww*w*

可以通过更新算法来解决这个问题,首先向左和向右填充当前行下方的单元格,然后向左和向右再次填充单元格当前行。假设state = v 表示我们来自上方,state = h1 表示这是第一个水平通道,state = h2 表示这是第二个水平通道。

您也许可以通过使用堆栈来避免重复访问单元格,但它更复杂。

伪代码:

array[][] // populated with towers, as shown in the question
visited[][] // starts with all false
// call at the position you're inserting water (at the very top)
define fill(x, y, state):
   if x or y out of bounds
          or array[x][y] == '*'
          or waterCount == 0
      return

   visited = true

   // we came from above
   if state == v
      fill(x, y+1, v) // down
      fill(x-1, y, h1) // left , 1st pass
      fill(x+1, y, h1) // right, 1st pass
      fill(x-1, y, h2) // left , 2nd pass
      fill(x+1, y, h2) // right, 2nd pass

   // this is a 1st horizontal pass
   if state == h1
      fill(x, y+1, v) // down
      fill(x-1, y, h1) // left , 1st pass
      fill(x+1, y, h1) // right, 1st pass
      visited = false // need to revisit cell later
      return // skip filling the current cell

   // this is a 2nd horizontal pass
   if state == h2
      fill(x-1, y, h2) // left , 2nd pass
      fill(x+1, y, h2) // right, 2nd pass

   // fill current cell
   if waterCount > 0
      array[x][y] = 'w'
      waterCount--

【讨论】:

    【解决方案3】:

    您有一个数组height,其中包含每列中地形的高度,因此我将创建该数组的副本(我们将其称为w 表示水)以指示每列中水的高度。像这样,您也摆脱了在转换为网格时不知道要初始化多少行的问题,您可以完全跳过该步骤。

    Java 代码中的算法如下所示:

    public int[] getWaterHeight(int index, int drops, int[] heights) {
        int[] w = Arrays.copyOf(heights);
        for (; drops > 0; drops--) {
            int idx = index;
            // go left first
            while (idx > 0 && w[idx - 1] <= w[idx])
                idx--;
            // go right
            for (;;) {
                int t = idx + 1;
                while (t < w.length && w[t] == w[idx])
                    t++;
                if (t >= w.length || w[t] >= w[idx]) {
                    w[idx]++;
                    break;
                } else { // we can go down to the right side here
                    idx = t;
                }
            }
        }
        return w;
    }
    

    即使有很多循环,复杂度也只有 O(drops * columns)。如果您预计会有大量的水滴,那么计算最高地形点 O(列)的空白空间数量可能是明智之举,然后如果水滴数量超过可用空间,则列高的计算变得微不足道O(1),但是设置它们仍然需要 O(columns)。

    【讨论】:

    • 您的算法似乎无法处理示例 2 的情况。而且我不太明白为什么在水滴落后返回一个数组,在这种情况下,很难打印。你能解释一下吗?
    • 例2是没问题的。要打印,您必须获取 max(max(terrain), max(water)) 才能知道您需要多少行。因为你从上到下打印它有点复杂。如果总行 - 行小于 col 处的水高,则字段 (row, col) 为空,否则如果总行 - 行大于 col 处的地形高度,则为水,否则为地形。
    • max(water)其实已经够得到行数了,忘了是累计的。
    • 试图从上到下打印,但它很复杂。但是,我可以垂直打印。(例如将浇注后的直方图旋转 90 度)
    【解决方案4】:

    您可以从下到上遍历 2D 网格,为每个水平连接的单元格创建一个节点,然后将这些节点串在一起形成一个链表,表示单元格的填充顺序。

    在第一行之后,你有一个水平运行,体积为 1:

    1(1)
    

    在第二行,你会发现三个运行,其中一个连接到节点 1:

    1(1)->2(1)    3(1)    4(1)
    

    在第三行,您会找到三个运行,其中一个连接运行 2 和 3; run 3 离加水的那一列最近,所以它排在第一位:

    3(1)->1(1)->2(1)->5(3)    6(1)    4(1)->7(1)
    

    在第 4 行中,您会找到两条运行,其中一条连接运行 6 和 7; run 6 离加水的那一列最近,所以它排在第一位:

    3(1)->1(1)->2(1)->5(3)->8(4)    6(1)->4(1)->7(1)->9(3)
    

    在第 5 行中,您会找到连接第 8 和第 9 行的运行;它们位于添加水的柱子的相对两侧,因此左侧的运行先行:

    3(1)->1(1)->2(1)->5(3)->8(4)->6(1)->4(1)->7(1)->9(3)->A(8)
    

    Run A 合并了所有的列,因此它成为最后一个节点,并被赋予无限量;任何多余的水滴都将被简单地堆叠起来:

    3(1)->1(1)->2(1)->5(3)->8(4)->6(1)->4(1)->7(1)->9(3)->A(infinite)
    

    然后我们按照它们列出的顺序填充运行,直到我们用完滴。

    【讨论】:

    • 这显然是一种通用方法,但不是 20 分钟的编码解决方案。
    【解决方案5】:

    这就是我的 20 分钟解决方案。每个水滴都在告诉客户它将停留在哪里,因此完成了艰巨的任务。(在您的 IDE 中复制粘贴)现在只需进行打印,但水滴正在占据它们的位置。看看:

    class Test2{
      private static int[] heights = {3,4,4,4,3,2,1,0,4,2,1};
      public static void main(String args[]){
      int wAmount = 10;
      int position = 2;
      for(int i=0; i<wAmount; i++){
        System.out.println(i+"#drop");
        aDropLeft(position);
      }
    }
    
    private static void aDropLeft(int position){
      getHight(position);
      int canFallTo = getFallPositionLeft(position);
      if(canFallTo==-1){canFallTo = getFallPositionRight(position);}
      if(canFallTo==-1){
        stayThere(position);
        return;
      }
      aDropLeft(canFallTo);
    }
    
    private static void stayThere(int position) {
      System.out.print("Staying at: ");log(position);
      heights[position]++;
    }
    
    //the position or -1 if it cant fall
    private static int getFallPositionLeft(int position) {
      int tempHeight = getHight(position);
      int tempPosition = position;
      //check left , if no, then check right
      while(tempPosition>0){
        if(tempHeight>getHight(tempPosition-1)){
          return tempPosition-1;
        }else tempPosition--;
      }
      return -1;
    }
    
    private static int getFallPositionRight(int position) {
      int tempHeight = getHight(position);
      int tempPosition = position;
      while(tempPosition<heights.length-1){
        if(tempHeight>getHight(tempPosition+1)){
          return tempPosition+1;
        }else if(tempHeight<getHight(tempPosition+1)){
          return -1;
        }else tempPosition++;
      }
      return -1;
    }   
    private static int getHight(int position) {
    return heights[position];
    }
    
    
    private static void log(int position) {
    System.out.println("I am at position: " + position + " height: " + getHight(position));
    }
    }
    

    当然可以优化代码,但这是我直接的解决方案

    【讨论】:

    • 一个小错误。您的第一个水滴始终位于位置 0。(我尝试了测试用例:您的高度数组,水 10 和水 20,位置:2)虽然您的输出很好,但最好打印图表而不是日志。跨度>
    • 如何定位到 0 位?
    • 我刚刚测试了它,即使第一滴落到了预期的位置
    【解决方案6】:
    l=[0,1,0,2,1,0,1,3,2,1,2,1]
    
    def findwater(l):
        w=0
        for i in range(0,len(l)-1):
            if i==0:
                pass
            else:
                num = min(max(l[:i]),max(l[i:]))-l[i]
                if num>0:
                    w+=num
        return w
    

    【讨论】:

      【解决方案7】:
      col_names=[1,2,3,4,5,6,7,8,9,10,11,12,13] #for visualization
      bars=[4,0,2,0,1,0,4,0,5,0,3,0,1]
      
      pd.DataFrame(dict(zip(col_names,bars)),index=range(1)).plot(kind='bar')   # Plotting bars
      
      def measure_water(l):
          water=0
          for i in range(len(l)-1):   # iterate over bars (list)
              if i==0:                # case to avoid max(:i) situation in case no item on left
                  pass
              else:
                  vol_at_curr_bar=min(max(l[:i]),max(l[i:]))-l[i]   #select min of max heighted bar on both side and minus current height
                  if vol_at_curr_bar>0:   # case to aviod any negative sum
                      water+=vol_at_curr_bar
          return water
      
      measure_water(bars)
      

      【讨论】: