SRM538-div1-3-SkewedPerspective


题目大意:
     有两种积木:1×1×1的彩色立方体积木,1×1×2的黑色积木。用int[] cube表示彩色积木的个数,每一个元素表示一种颜色的积木个数,用int B表示黑色积木的个数。
     用这些积木的一部分堆砌成若干个方柱(不超过w个),每个方柱都是1×1的截面(高度不一定相同),把这些方柱从左到右整齐排列好,然后整体向左侧投影形成投影带,只要积木的左侧没有被其他积木挡住,就会把自己的颜色投影到对应高度。把投影带看作是一个个1×1大小的格子组成,从下到上,可以看成一系列颜色的格子的排列(同一块黑色积木投影出的2×1的影子看成两个)。问任意堆砌这些积木,总共可以形成多少种不同的投影(取1,000,000,007模)?(不同投影:投影的高度不同,或者某一相同高度的格子颜色不同)
     数据规模:w为[1,8], 各种颜色积木数均为[1,50], 彩色积木的总数[0,50], 黑色积木数[0,10] 
     
思路:
     定义cNum为立方体积木总数。
     由于立方体积木的色彩与积木的堆砌方式之间没有任何关系,所以可以把积木堆砌过程和立方体染色分成两步来考虑。首先,假设所有的立方体积木都是白色的,计算可以投影成多少种黑白格子的投影带;然后,对每一种投影带,根据白色格子的数量,计算其不同颜色组合的个数。所以该问题可以分成两个子问题来求解。
  1. 假设所有的立方体积木都是白色时,那么包含x个白色格子的不同投影带有多少种,定义为f(x)。其中x取值为[0,cNum]。
  2. 用彩色立方体排成高度为x的方柱,可以形成多少种颜色不同的方柱, 定义为g(x)。
      通过求解这两个子问题,我们可以得到该问题的答案为f(0)*g(0)+f(1)*g(1)+...+f(cNum)*g(cNum)。
     
     第二个子问题相对比较容易,先解决这个问题。使用dp算法,定义 perNum(i,x)为:只考虑前i种彩色积木,可以形成多少种高度为x的不同方柱。状态转移: perNum(i,x)已知的情况下,考虑第i+1种颜色的方块,方块个数为cube[i],选择任意个数(假设为k)方块加入到方柱中, 这k个方块可以与之前的x个方块形成C(x+k,k)种组合,那么我们可以得到下式:
     perNum(i+1,x+k)+= perNum(i,x)*C(x+k,k)
其中所有 perNum(i+1,*)一开始都被初始化为0;C(A,B)为二项式系数。那么f(x)=perNum(n,x),其中n为色彩数。
   
   第一个问题的求解需要对积木堆砌以及投影进行进一步分析。每一种投影结果,有可能有多种不同的堆砌方式,定义最优的堆砌方式为使用的积木数最少的堆砌方式(黑色积木看成2个)。由于那些被挡住的积木不会对投影结果产生影响,我们称这些积木为“填充积木”,而相应的具有投影的积木为“有效积木”。如果两个堆砌方式中,只有填充积木不同,那么认为这两种堆砌方式是相同的。对于一个特定的投影结果,其最优的堆砌方式必然只有一种(相同的堆砌方式统称为一种)。因为,在最优堆砌中,我们总是遵循一个原则,能不换方柱就不换方柱,要换方柱就早换方柱。只有在投影中出现连续的奇数个黑色格子的时候才需要换方柱,并且要早换,从第一个黑色格子开始就应该换到下一个方柱。根据该分析,我们只需要求解投影中白色格子数为x时的不同积木堆砌方式即可。由于在模拟积木堆砌的过程中我们需要判断一种堆砌方式是否是可行的(积木数是否足够),所以我们必须关心几个因素:
  • 有效白色积木数
  • 有效黑色积木数
  • 当前需要的填充积木数,以及填充空间中至少需要多少个白色积木(如果一个需要填充的空间高度是奇数,那么至少需要一个白色积木)
  • 当前投影的高度
  • 当前的方柱数

可以使用dp算法来求解,由于直接使用以上几个因素的参数来表示的状态不够紧凑,空间上无法满足,所以改用等价的以下几个参数来描述:
  • 有效白色积木数c。
  • 只投影半截的有效黑色积木数b1。由于投影半截的黑色积木是换方柱的标志,所以我们可以根据这个值得到当前的方柱数这个信息。
  • 全部有投影的黑色积木数b2。根据这三个参数可以描述当前投影的高度。
  • 必须使用白色积木的填充空间e1。这个参数描述至少需要多少个白色积木来进行填充。
  • 剩下的填充空间e2。由于剩下的填充空间必然是偶数的,为节约空间,我们实际记录的是剩下填充空间除以2的值。
  • 在最优堆砌方案中,连续奇数个黑色格子中,第一个格子的投影必然是b1中的黑色积木产生的(有个例外情况下面说明),所以在状态转移过程中需要注意b1的黑色积木不能放在其他有效黑色积木之上(因为这样就不是最优堆砌了,会产生投影的重复计算)。我们还需要一个状态来描述当前最高位置的积木是否是黑色积木,isBlack,取值为0或1。
   根据以上描述的参数,实现一个具有6维状态参数的dp算法,状态值为该组状态参数所描述的不同堆砌方式数。起始状态所有参数都是0, 状态值 为1。状态转移过程可以有三种选择:
  • 当前方柱增加一个白色方块,即c加1
  • 当前方柱增加一个黑色方块,即b2加1
  • 如果isBlack为0,换一个方柱,增加一个半投影的黑色方块,即b1加1。同时需要根据当前的高度考虑e1和e2值的变化。
   有一个特例需要关注一下,如果投影中出现从第一个格子开始的连续奇数黑色格子(必然>=3),那么是无法通过以上三种操作来实现的(因为无法让第一个黑色方块被遮住一半)。这个时候最优的方案为,第一个方柱包含一个黑色方块,从第二个方柱的高度1开始再摆放一个黑色方块。即b1加1,b2加1,e1加1。
   
   在进行每一步操作时,我们都需要先判断该操作是否是有效的,有效操作之后的状态必须满足以下几个条件:
  • 白色方块数未超标,即:c + e1 <= cNum ;
  • 黑色方块数为超标,即:b1 + b2 <= B ;
  • 方柱数为超标,即:b1 < w && e1 < w;
  • 总方块数未超标(包含填充部分),即:c + e1 + 2 * (e2 + b1 + b2) <= cNum + 2 * B
   以上各参数的最大值:c为cNum, b1为min(w - 1, B), b2为B, e1为w-1, e2为cNum / 2 + B, isBlack为2。
   即便是最大的数据规模下,状态总数为51 * 8 * 11 * 8 * 34 * 2= 2,441,472。使用long来存储状态值基本没有压力!由于状态转移过程是O(1)复杂度,所以计算时间也木有压力!
   最终g(x)为所有c=x的状态值的和。在具体实现中,可以在dp过程中就把最终结果累加起来(如下java代码所示)。
   



Java代码:

public class SkewedPerspective {
    final int MOD = 1000000007;
    int       cNum, B , w ;
    //int[a][b]: B(a, b)
    private int[][] binomial(int n) {
        int[][] res = new int[n + 1][n + 1];
        for (int i = 0; i <= n; ++i) {
            res[i][0] = 1;
            for (int j = 1; j <= i; ++j) {
                res[i][j] = (res[i - 1][j - 1] + res[i - 1][j]) % MOD;
            }
        }
        return res;
    }
    //立方块个数在每一种取值情况下的排列数
    private long[] solve1(int[] cubes) {
        int[][] bi = binomial(cNum );
        //f(已考虑的颜色数,立方块个数)->排列数
        long[] dp = new long[ cNum + 1];
        dp[0] = 1;
        for (int i = 0; i < cubes.length; ++i) {
            long[] next = new long[ cNum + 1];
            for (int n = 0; n <= cNum; ++n) {
                for (int j = 0; j <= cubes[i]; ++j) {
                    if (n + j <= cNum ) {
                        next[n + j] += dp[n] * bi[n + j][j];
                        next[n + j] %= MOD;
                    }
                }
            }
            dp = next;
        }
        return dp;
    }
    private boolean isCubeValid(int c, int e1) {
        return c + e1 <= cNum ;
    }
    private boolean isBlackValid(int b1, int b2) {
        return b1 + b2 <= B ;
    }
    private boolean isWidthValid(int b1, int e1) {
        return b1 < w && e1 < w;
    }
    private boolean isAllValid(int c, int b1, int b2, int e1, int e2) {
        return c + e1 + 2 * (e2 + b1 + b2) <= cNum + 2 * B && isCubeValid(c, e1)
                && isBlackValid(b1, b2) && isWidthValid(b1, e1);
    }
    private int height(int c, int b1, int b2) {
        return c + b1 + b2 * 2;
    }
    private int solve2(int[] cubes, long[] perNum) {
        //(cubeCount, singleBlackCount, doubleBlackCount, singleEmptyCount, doubleEmptyCount, isLastBlack)
        long[][][][][][] dp = new long[ cNum + 1][w ][B + 1][w][cNum / 2 + B + 1][2];
        long res = 0;
        dp[0][0][0][0][0][0] = 1;
        for (int c = 0; c < cNum + 1; ++c) {
            for (int b1 = 0; b1 < w; ++b1) {
                for (int b2 = 0; b1 + b2 < B + 1; ++b2) {
                    for (int e1 = 0; e1 < w; ++e1) {
                        for (int e2 = 0; e2 < cNum / 2 + B + 1; ++e2) {
                            for (int isBlack = 0; isBlack <= 1; ++isBlack) {
                                if (dp[c][b1][b2][e1][e2][isBlack] == 0)
                                    continue;
                                long t = dp[c][b1][b2][e1][e2][isBlack];
                                if (height(c, b1, b2) != 0) {
                                    res += t * perNum[c];
                                    res %= MOD;
                                }
                                //add a cube
                                if (isAllValid(c + 1, b1, b2, e1, e2)) {
                                    dp[c + 1][b1][b2][e1][e2][0] += t;
                                    dp[c + 1][b1][b2][e1][e2][0] %= MOD;
                                }
                                //add double Black
                                if (isAllValid(c, b1, b2 + 1, e1, e2)) {
                                    dp[c][b1][b2 + 1][e1][e2][1] += t;
                                    dp[c][b1][b2 + 1][e1][e2][1] %= MOD;
                                }
                                //add single Black
                                if (height(c, b1, b2) == 0) {
                                    if (isAllValid(c, b1 + 1, b2 + 1, e1 + 1, e2)) {
                                        dp[c][b1 + 1][b2 + 1][e1 + 1][e2][1] += t;
                                        dp[c][b1 + 1][b2 + 1][e1 + 1][e2][1] %= MOD;
                                    }
                                } else if (isBlack == 0) {
                                    int h = height(c, b1, b2);
                                    int ne1 = e1 + (h - 1) % 2;
                                    int ne2 = e2 + (h - 1) / 2;
                                    if (isAllValid(c, b1 + 1, b2, ne1, ne2)) {
                                        dp[c][b1 + 1][b2][ne1][ne2][1] += t;
                                        dp[c][b1 + 1][b2][ne1][ne2][1] %= MOD;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return (int ) res;
    }
    public int countThem(int[] cubes, int B_, int w_) {
        cNum = 0;
        for (int i = 0; i < cubes.length; ++i) {
            cNum += cubes[i];
        }
        B = B_;
        w = w_;
        return solve2(cubes, solve1(cubes));
    }
}



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值