背景
很多视频流app都有大小卡混排的页面,下面举4个例子。
在实现上,整个视频流的数据可能是一个一维列表的形式,比如[大卡,小卡,小卡,大卡,小卡,小卡...],然后客户端再把这个一维列表渲染成一行大卡,几行小卡的形式。不是任意一个一维列表都能正确渲染出页面,大卡在列表中的位置需要满足一定的要求。本文介绍针对固定每隔n行出一个大卡的情况,怎样给出一个合理的一维列表。
我们先定义下面几个变量:
numPerRow:每行出几个小卡。
firstRow:第一个大卡出现在第几行,从1开始数。
intervalRow:两个大卡之间有多少行小卡。
那么第一个大卡的下标为(firstRow−1)×numPerRow,之后每个大卡的下标间隔为numPerRow×intervalRow+1。
因此大卡的下标集合为BigI={(firstRow−1)×numPerRow+n×(numPerRow×intervalRow+1)∣n∈Z≥0}
在实现上我们有几个问题。视频流是分次拉取的,所以我们要知道这一刷中哪几个是大卡。一种实现方式是每次下发一个周期,也就是每次下发numPerRow×intervalRow+1个视频,这样每一刷的第一个都是大卡,且仅有一个大卡。但是这样的方式限制了每一刷返回多少个视频,而这个值是一个会影响体验的重要指标,经过多轮实验,我们终于找到一个完美的值,我们不能因为要实现大小卡混发而随意改动这个值。所以我们要用较为复杂的方法计算每一刷中哪几个视频是大卡。同时如果在一刷的最后一行是小卡,而数量不满numPerRow,就会出现右下角缺角,这也是很不好的体验,我们要避免。所以我们不能保证每一刷返回的视频数相同,我们采用把最后缺角的一行过滤掉的办法,最大限度减少对每一刷返回视频数量的影响。于是我们面临两个问题,一个是要感知本刷哪几个视频是大卡,一个是要感知一刷最后一行有几个视频,如果出现缺角就执行过滤。下面我们说说怎样解决这两个问题。
1. 本刷哪几个视频是大卡。
视频流是分次拉取的,一般每次请求,客户端会带一个offset过来,表示已经出了多少个视频,包括大卡与小卡。给定一个offset,我们要知道这一刷中哪几个是大卡。一种实现方式是每次下发一个周期,也就是每次下发numPerRow×intervalRow+1个视频,这样每一刷的第一个都是大卡,且仅有一个大卡。但是这样的方式限制了每一刷返回多少个视频,而这个值是一个会影响体验的重要指标,经过多轮实验,我们终于找到一个完美的值,我们不能因为要实现大小卡混发而随意改动这个值。所以我们要用较为复杂的方法计算每一刷中哪几个视频是大卡。
这个问题基本等同于问截至目前为止,下一个大卡在哪个位置。再后面的大卡位置通过加间隔就能获得。
也就是给定一个offset,我们要找到一个最小的i≥offset使得i∈BigI。
令i=(firstRow−1)×numPerRow+ni×(numPerRow×intervalRow+1),我们有
(firstRow−1)×numPerRow+ni×(numPerRow×intervalRow+1)≥offset,且
(firstRow−1)×numPerRow+(ni−1)×(numPerRow×intervalRow+1)<offset
我们把问题抽象一下,变成
a×ni+b≥c且a×(ni−1)+b<c(a>0,ni∈Z),
于是ni≥ac−b且ni−1<ac−b。
令x=ac−b,也就是说ni是最小的大于等于x的整数,这就是数学上天花板的定义,记为
ni=⌈x⌉=⌈ac−b⌉=⌈(numPerRow×intervalRow+1)offset−(firstRow−1)×numPerRow⌉。
只要算出ni我们就能算出i。虽然Go的标准库提供了math.Ceil函数来计算天花板,但是它的入参是float64,浮点数存在精度问题。如果一个数x的精确值为1,那么⌈x⌉=1。但是如果表示用float64表示,有可能x=1.00001,math.Ceil(x)=2,导致结果差了1,这样的误差显然是不可接受的,所以我们不能用这个方法来计算天花板。
为了算天花板,我们请出它的兄弟,地板。一个实数r的地板f,是一个满足f≤r的最大的整数,记作f=⌊r⌋。我们发现在编程语言中,有一个与天花板和地板有千丝万缕关系的运算,整数除法。在包括Go在内的很多编程语言中,如果a和b均为整数类型,a/b的结果也为整数类型,其运算规则为计算ba的值,保留整数部分,丢弃小数部分。下文用/int来表示整数除法。事实上,
当ba≥0时,a/intb=⌊ba⌋;
当ba≤0时,a/intb=⌈ba⌉。
这样当ba≤0时,我们会算天花板了,但当ba>0时,我们还是不会算。但其实我们可以用地板来计算天花板,下面我们介绍怎样做。
我们证明当a和b均为正整数时,⌈ba⌉=⌊ba+b−1⌋。
当ba为整数时,⌈ba⌉=ba。
且ba≤ba+b−1<ba+1,
因此⌊ba+b−1⌋=ba=⌈ba⌉。
当ba不为整数时,我们把a写作a=k×b+c,其中k与c为整数,且0<c<b。那么ba=k+bc。
易得⌈ba⌉=k+1。
ba+b−1=k+1+bc−1。
易得k+1≤k+1+bc−1<k+2,
因此⌊ba+b−1⌋=k+1=⌈ba⌉。
证完。
这样我们就打通了整个计算流程。
当offset≤(firstRow−1)×numPerRow时,实际上表示当前从未展示过大卡,所以i=(firstRow−1)×numPerRow。用上面公式计算会有问题,因为上面的讨论把ni的范围从非负整数放大到所有整数。
当offset>(firstRow−1)×numPerRow时,
ni=⌈(numPerRow×intervalRow+1)offset−(firstRow−1)×numPerRow⌉=⌊(numPerRow×intervalRow+1)offset−(firstRow−1)×numPerRow+(numPerRow×intervalRow+1)−1⌋=(offset−(firstRow−1)×numPerRow+numPerRow×intervalRow)/int(numPerRow×intervalRow+1);
i=(firstRow−1)×numPerRow+ni×(numPerRow×intervalRow+1)。
2. 本刷应该返回多少个视频
如果在一刷的最后一行是小卡,而数量不满numPerRow,就会出现右下角缺角,这也是很不好的体验,我们要避免,所以我们不能保证每一刷返回的视频数相同。我们采用把最后缺角的一行过滤掉的办法,最大限度减少对每一刷返回视频数量的影响。我们用maxLimit表示每一刷最多出多少个视频。
也就是给定offset与maxLimit,我们要求一个最大的小于等于offset+maxLimit−1的下标j使得第j个视频要么是一个大卡,要么所在行有numPerRow个小卡。
如果offset+maxLimit−1<(firstRow−1)×numPerRow,也就是到最后一个大卡都没出,那么j=mj×numPerRow−1≤offset+maxLimit−1,
且j=(mj+1)×numPerRow−1>offset+maxLimit−1。
那么用跟上面类似的套路,mj=⌊numPerRowoffset+maxLimit⌋=(offset+maxLimit)/intnumPerRow;
j=(offset+maxLimit−1)/intnumPerRow×numPerRow。
下面讨论offset+maxLimit−1≥(firstRow−1)×numPerRow的情况。我们可以先求小于等于offset+maxLimit−1的最大的大卡的位置l,
也就是l=(firstRow−1)×numPerRow+nl×(numPerRow×intervalRow+1)≤offset+maxLimit−1,且
(firstRow−1)×numPerRow+(nl+1)×(numPerRow×intervalRow+1)>offset+maxLimit−1,
nl=⌊numPerRow×intervalRow+1offset+maxLimit−1−(firstRow−1)×numPerRow⌋=(offset+maxLimit−1−(firstRow−1)×numPerRow)/int(numPerRow×intervalRow+1)。
那么j=l+mj×numPerRow,0≤mj≤intervalRow且m为整数。
同理,mj=⌊numPerRowoffset+maxLimit−1−l⌋=(offset+maxLimit−1−l)/intnumPerRow。
这样j就能计算出来了。j−offset+1就是这一刷要出的视频的数量,offset+maxLimit−1−j就是要裁掉的视频的数量。
3. 第n行的第一个视频的index
除了大卡与小卡两种,我们有时还有插入另外一些类型的大卡的需求。我们的做法是这些特殊大卡插入到某一行中,单独占一行,不影响大卡小卡的位置计算。如果要把特殊大卡插入到第n行(从1开始算),我们需要计算它应该在整个视频流的第k位,也就是要求第n行的第一个视频的index。
我们分情况来计算。
当n<firstRow时,前面全是小卡,所以k=(n−1)×numPerRow。
当n≥firstRow时,我们计算有几个完整周期(一个大卡+intervalRow行小卡),用completeBlocks表示,以及最后的不完整周期有几行,用remainingRows表示。于是
completeBlocks=(n−firstRow)/int(intervalRow+1)
remainingRows=(n−firstRow)mod(intervalRow+1)
当remainingRows=0时,k=(firstRow−1)×numPerRow+completeBlocks×(intervalRow×numPerRow+1);
当remainingRows>0时,k=(firstRow−1)×numPerRow+completeBlocks×(intervalRow×numPerRow+1)+(remainingRows−1)×numPerRow+1。