SRM534-div2-3-EllysFiveFriends


题目大意:
     5个整数(均是1到10000的范围)从左到右编号为0,1,2,3,4后围成一个圈,对这些整数进行一系列的操作,每一次操作选取两个相邻的非零整数进行如下之一的运算:
  • a. 如果两个整数都是奇数,那么可以将两个数各自减一;
  • b. 两个数都除2(奇数除2后取整)
     每一次操作可以表示为:(i j k), 表示对编号为i和j的数字进行k运算。
     如果最后5个整数都变成0,那么恭喜你,你赢了!给定这5个整数,问有多少种不同的操作序列能赢?(结果取1,000,000,007的模)
     注:只要两个操作序列中有一个对应的操作(i,j,k)不同,则为不同的操作序列。

思路:
     这道题目的描述有点蛋疼,不复杂的题意硬是描述了那么长,加入了苹果汁、橙汁等各种与题目无关的概念,这让我们这种英文不好的人情何以堪。。。还好有有道词典在手=.=
     这种题目的第一感觉就是动态规划算法,令f(s)表示s状态下达到胜利的不同操作序列数。状态转移方程为f(s)=sum(f(s1)+f(s2)+...),其中s1,s2...表示s状态所能达到的所有状态(这里取模的计算省略掉)。一开始粗略的估计,5个整数最大值都可以达到是10000,如果中间状态每个整数都可能取[0,10000],那岂不是有10000^5个状态,吓了一跳。但是仔细发掘题中两种运算的内涵会发现其实根本没有那么多。
     先分析一下这两个运算的特点。对于一个数n,如果n为偶数,那么可以变化为n/2; 如果n为奇数,那么可以变化为n/2或n-1。如果变为n-1,那么该数变为偶数,下一步只能变为(n-1)/2,由于n为奇数那么n/2和(n-1)/2是相等的。所以可以预见每一个整数只能变化为非常有限的其他整数,n只能变为 n, [n-1], n/2, [n/2-1], n/4, [ n/4-1], n/8, [n/8-1] ...,0 ,其中中括号中的数只有该数值是偶数时才会出现。该整数序列的长度为 2*log2(n), 所以中间状态最多只需要保存 (2*log2(n))^5, 当n=10000时为13,255,651,可以接受。
     这道题最大的难度其实是在于dp算法所要保存的中间状态太过复杂,包含了5个最大取值可以是10000的整数,如何使用高效的方法记录这些状态是解题的关键。一开始笔者低估了题目的数据规模,直接用包含5个整数的对象来记录状态,结果超时了。后面想过将5个整数拼接成long型的整数来表示,写了大部分代码才发现5个10000拼接起来根本不是long能表示的了的。最后只能求助于官方题解了。
     实际上,由于我们前面说过每个整数变化的序列其实相当的有限,我们可以对这些序列中的数值进行编号,然后再生成一个编号到实际数值的表。在求解过程中只需要将这些编号作为状态进行记录即可。
     根据题解,我的解题策略是首先根据5个整数生成一个表格values[5][27],values[i]表示第i个整数所能变化到的整数的列表。列表生成的方法为:对于整数n,n为列表的第一个元素。如果n为奇数则,则分别按顺序将n-1和n/2塞入列表中;如果n为偶数,则将n/2塞入列表中。后面对n/2进行类似操作,直到塞入的数值变为0为止。这种方法创建的表格有如下特点:对于表格中的任何一个元素x,如果x是偶数,那么他只能变化为 后面的那个数值;如果x是奇数则可以变化为它后面的两个数值(前一个为减1后一个为除2)。根据该列表我们在状态中只需要保存列表的序号即可,即可以用一个多维数组int[27][27][27][27][27]来保存中间状态值。通过这种方法可以在规定的时间通过所有case。
     关于整数变化序列最大有多长:从二进制表示的角度看题中两种方法,奇数减1可以理解为将二进制表示中的最后一个1抹去,除2可以理解为二进制向右移一位。所以,每一次运算相当于从该整数的二进制表示中去除一位或者抹去一个1,那么10000以内的整数中二进制表示的“位数+1的个数”最大的那个数拥有最长的列表,这个数为8191(二进制为1111111111111,比它更优的最小的数为10111111111111,值为12287大于10000),列表长度为26。以下代码中列表的长度取27是因为按代码中的计算方法最后会产生两个0(为什么?)。如果需要进一步优化性能可在最后做判断以减少一个0。

Java代码:
public class EllysFiveFriends {
    static final int MOD    = 1000000007;

    int[][][][][]    mem    = new int[27][27][27][27][27];
    int[][]          values = new int[5][27];

    int dpf(int[] s) {
        if (mem[s[0]][s[1]][s[2]][s[3]][s[4]] >= 0) {
            return mem[s[0]][s[1]][s[2]][s[3]][s[4]];
        }
        int sum = 0;
        for (int i = 0; i < 5; ++i) {
            sum += values[i][s[i]];
        }
        if (sum == 0) {
            mem[s[0]][s[1]][s[2]][s[3]][s[4]] = 1;
            return 1;
        }
        long res = 0;
        for (int i = 0; i < 5; ++i) {
            int i1 = i;
            int i2 = (i + 1) % 5;
            int v1 = values[i1][s[i1]];
            int v2 = values[i2][s[i2]];
            if (v1 == 0 || v2 == 0) {
                continue;
            }
            if (v1 % 2 == 1 && v2 % 2 == 1) {
                s[i1]++;
                s[i2]++;
                res += dpf(s);
                s[i1]--;
                s[i2]--;
            }
            s[i1] += v1 % 2 + 1;
            s[i2] += v2 % 2 + 1;
            res += dpf(s);
            s[i1] -= v1 % 2 + 1;
            s[i2] -= v2 % 2 + 1;
        }
        res = res % MOD;
        mem[s[0]][s[1]][s[2]][s[3]][s[4]] = (int) res;
        return (int) res;
    }

    public int getZero(int[] nums) {
        for (int[][][][] a : mem)
            for (int[][][] b : a)
                for (int[][] c : b)
                    for (int[] d : c)
                            Arrays.fill(d, -1);
        for (int i = 0; i < 5; ++i) {
            int j = 0;
            values[i][j++] = nums[i];
            while (nums[i] != 0) {
                if (nums[i] % 2 == 1) {
                    nums[i]--;
                    values[i][j++] = nums[i];
                }
                nums[i] /= 2;
                values[i][j++] = nums[i];
            }
        }
        int[] s = new int[5];
        return dpf(s);
    }
}

   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值