瀑布流背后的算法问题

2,998 阅读5分钟

前言

之前在看公众号时,看到一个比较有意思的问题,讲的是一个面试过程,大致的问题过程如下:

A:你有写过瀑布流吗?

B:我写过等宽瀑布流。实现是当用户拉到底部的一定高度的时候,向后端请求一定数量的图片,然后再插入到页面中。

A:那我问一下,如何让几列图片之间的高度差最小?

B:这个需要后端发来的数据里面有图片的高度,然后我就可以看当前高度最小的是哪里列,将新图片插入那一列,然后再看看新的高度最小的是哪一列。

A:我觉得你没有理解我的问题,我的意思是如何给后端发来的图片排序,让几列图片之间的高度差最小?

B:(想了一段时间)对不起,这个问题我没有思路。

这个题我觉得很有意思,于是就对针对这个问题,展开了一段比较难忘的探究之旅

img

问题分析

简单转化一下问题,这个问题可以被描述为将任意数分成特定的组,使得每组的和尽量相等

那么怎么解决这个问题呢?

我之前解决这类问题的思路大抵是先找规律,找到内部的共同点,继而进行推算,所以首先我就按这个方法尝试探究一下这个问题

不考虑分成一组的情况,我们直接考虑两组

在两组的情况下,很容易可以想到只要将其中一组尽可能拿到总和的1/2即可

那么这个问题就被转化为了01背包问题

0-1背包问题

条件:数组中的数就是背包问题的weight值,数组中的数也是背包问题的value值,即二者一样。

问题:背包里装哪些物品,使得其价值之和最接近总价值的一半。

01背包的处理取决于拿与不拿

简单陈述通过动态规划来求解0-1背包问题的思路。

用f(1,2,3,4....i)(j)表示背包为j时能在i个物品中取出的最大价值

用w(i),v(i)表示第i个物品的重量个价值

拿的表达式 f(1,2,3....i-1)(j-w(i))+v(i)

不拿的表达式 f(1,2,3....i-1)(j)

获得状态转移方程f(i)(j)=max(f(i-1)(j-w(i))+v(i),f(i-1)(j))       1,2,3...i省略成i

chooseOrNot=(List, halfNum)=>{
        let len = List.length
        let f = new Array(len)
        f[-1] = new Array(halfNum+1).fill(0) //使f[0-1][j]存在
        let selected = []
        for(let i = 0 ; i < len ; i++){ 
            f[i] = [] //创建二维数组 
            for(let j=0; j<=halfNum; j++){ 
                if( j < List[i] ){ 
                    f[i][j] = f[i-1][j] //物体比背包大
                }else{
                    f[i][j] = Math.max(f[i-1][j], f[i-1][j-List[i]]+List[i])
                }
            }
        }
        let j = halfNum, w = 0
        for(let i=len-1; i>=0; i--){
            if(f[i][j] > f[i-1][j]){
                selected.push(i)
                j = j - List[i]
                w +=  List[i]
            }
        }
        return [f[len-1][halfNum], selected]
    }

"三体问题"

运用上述的方法其实就是回溯法,我们能不能获得将数组均分为三组的最优解呢?即把上述的一半改为1/3,我将之前的例子[2,3,4,5,6]带入,每次尽可能得到了 [2,4],[5],[3,6]很显然这并不是最优解。

然后就是对于将任意数分成三组,使得每组的和尽量相等这个问题这个问题的漫长探究。。。

我大概被困扰了两天,我将其命名为三体问题,无序而且混乱。

img

NP完全问题

询问了我的组合数学老师,老师让我看看NP完全问题,查阅一番资料之后,我发现这个问题可能是一个NPC问题,不看不知道一看吓一跳,这个问题居然能直接顶到数学的天花板。作为千禧难题的首位,对于这类问题,我们或许无法找到最优解,但是通过计算近似解,往往能大大降低时间复杂度。

贪心算法

回到'三体问题',贪心算法在数据比较均匀的时候不失为一种优秀解决方法

模仿儿童为游戏选择团队的方式的问题的一种方法是贪婪算法,其以降序迭代数字,将它们中的每一个分配给具有较小总和的任何子集。该方法的运行时间为O(n log n)。当该集合中的数字与其基数大小相同或更小时,该启发式在实践中运行良好,但不能保证产生最佳可能的分区。

也就是我们常写的瀑布流,当然需要排序一下,比较简单直接拓展到n了

threeBody = (array,n) => {
	array.sort((a, b) => {
		return b - a
	})
	let list=[]
	for (let j = 0; j < n; j++) {
		list[j] = []
	}
	let arr=[]
	for (let i = 0; i < array.length; i++) {
		for (let j = 0; j < n; j++) {
			arr[j]=sum(list[j])
		}
		min = Math.min(...arr)
		for (let j = 0; j < n; j++) {
			if(min==arr[j]){
				list[j].push(array[i])
				break
			}
		}
		}
	return(list,arr)
}

当然这类问题根据数据的规模和数据范围有不同解法。例如针对含有大数的数据,针对单体大于所取平均数的,可以先单独分做一类,再对剩下的数据进行处理。再比如这篇论文所提到的分治求解法在数据较少的时候未尝不是一种高效解。

写在后面

这是一道很有意思的算法题,我们可以尝试用不同的算法,来尽量获得最省时准确率高的近似解。不知道大家伙有没有更好的办法呢?