POJ 1014: Dividing

来源:互联网 发布:sas数据分析大赛含金量 编辑:程序博客网 时间:2024/06/07 02:03

写在前面: 本题很有意思, 因此我在编程前后和撰写本文前, 在网上查询并阅读了大量关于本题的解题报告. 其中有两种似是而非但提交后却能够 AC 的解法引起了我的兴趣, 一是原作者宣称的 “DFS 解”, 二是原作者宣称的 “扩展的找零问题解”. 我对这两种解法的源代码进行了仔细的分析, 找到了它们的问题之所在. 下面我会先对这两种错误解法进行分析和讨论, 然后再给出正确的思路.

错误解法 1: 按照价值从大到小的顺序尝试凑数 (不回溯).
由于裁判的测试数据不够全面, 基于这种思路设计出的程序在提交之后是能够 AC 的. 然而有一个典型的反例: “0 0 3 0 3 1”, 这是一个可分的组合, 但用上述思路则会判定为不可分.
事实上, 对于绝大多数编程者来说, 这种思路在通常应该会被第一时间在头脑中否决. 然而一些编程者在对 DFS 的尝试中逻辑出现了错误, 导致程序实质上实现的正是上述思路. 由于能够 AC, 所以编程者会误认为自己编写出了正确的 DFS 版程序. 下面提供原文链接, 供有兴趣自行研究的读者参考.

  • http://blog.csdn.net/lyy289065406/article/details/6661449
  • http://blog.csdn.net/li4951/article/details/7434598

错误解法 2: 循环尝试最大化选取单种价值的石头 (有回溯).
具个具体的例子: 首先尝试以 half 价值为限, 最大化选取价值为 6 的石头 (全选或选择 int( half / 6 ) 块, 设为 n6) 再以 half - n6 价值为限, 最大化选取价值为 5 的石头, 以此类推. 若发现无法凑出 half 价值, 则回溯, 尝试以 half 价值为限, 最大化选取价值为 5 的石头, 以此类推. 编程者认为这种方法是 “扩展的找零问题解法”.
事实上, 这种解法同样是错误的, 原因在于这种解法从根本上讲是基于一个命题, 即 “对于任意一个价值限度, 如果能够凑得出, 则必定存在一种最大化选取某一种价值的石头的凑法”. 此命题的反例非常容易找到: “0 0 0 2 2 2”. 可是基于上述思路编写的程序提交后仍能 AC, 所以又一次使编程者误认为自己的思路是正确的. 下面提供原文链接, 供有兴趣自行研究的读者参考.

  • http://poj.org/showmessage?message_id=346478

解题要点 1: 选择核心算法.
尝试提交过 DFS 版的代码就会发现无法达到题目要求的时限, 因此 DP 才是这道题的唯一 (严谨点说的话是 “很可能是唯一”) 可行算法. 大致流程如下:

  1. 若石头的总价值为奇数, 则立即判定为不可分.
  2. 建立长度为 6 × 20000 / 2 + 1 = 60001 的旗标阵列用于标识给定的石头能够组合出的价值, 并设位置 0 处的旗标为 true (一块也不选, 即可组合出价值为 0 的组合).
  3. 遍历每块石头, 设当前石头价值为 value (1 ≤ value ≤6), 嵌套逆序遍历旗标阵列, 设当前序号为 i. 若某个序号 i 对应的旗标为 true (用之前的石头可以组合出该价值), 则设定序号 i + value 对应的旗标为 true (价值 i 加上当前石头的价值). 此步骤可以进行多项优化以提升算法效率, 详见后续的解题要点.
  4. 若总价值的一半所对应的旗标为 true, 则判为可分; 否则判定为不可分.

解题要点 2: 对每种价值的石头进行二进制组合.
一个广为人知的小原理:

对于任一满足 2k<n<2k+1 的自然数 n, 将其拆分成下列数列之和:

20,21,...,2k,n2k

则 1 ~ n 中的任一自然数都可以用上述数列中某些项之和来表示.

每种价值的石头原本是以单体的形式存在的. 利用上述原理, 即可使每种价值的石头以 “装箱” 的形式存在, 同时保证这些 “石头箱” 所能够生成的价值组合完全等同于原本的石头单体所能够生成的价值组合. 如此一来, 解题要点 1 中算法流程第 2 步的第一重循环的次数便会大幅减少.

解题要点 3: 剪枝.
仅根据前面介绍的两个解题要点来编程, 就已能够达到相当快的运行速度 (请参考 “极简版代码”). 然而实际上还是能够根据题目的特点在前两个解题要点的基础上做进一步的优化. 具体如下:

  • 对石头进行二进制组合后, 对 “石头箱” 价值阵列进行从小到大的排序, 为剪枝做准备.
  • 设总价值的一半为 half, 若价值阵列的最后一项大于 half , 则立即判定为不可分.
  • 设第一重循环已进行到价值为 value 的某个 “石头箱”. 在第二重循环之前, 检查序号为 half - value 的旗标 (排序的目的之一), 若为 true, 则立即判定为可分.
  • 尽量降低第二重循环的初始值以减少循环次数. 设初始值为 m, 则 m 的初值为 0. 每次第二重循环结束后, 修改初始值为 m = min( m + value, half ). 由于之前已对价值进行了从小到大的排序, 因此 m 的增长速度达到了最慢.

经过上述优化, 提交后程序的运行时间少于 1 ms. 请参考 “优化版代码”.

极简版代码如下:

在这一版的代码中, 我希望尽量缩减代码行数, 因此就连解题要点 2 中提到的优化方案都没有完全采用. 具体地说, 算法流程的第 1 步被后移, 算法流程第 3 步中的 “逆序遍历” 无用武之地. 其运行速度虽能够轻松达到裁判的要求, 但还是略慢于优化版的代码. 尽管如此, 我相信这一版的代码一定可以从某种角度启发到读者的思路.

#include <iostream>#include <bitset>using namespace std;int main() {    const int REGROUP[15] = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384 };    bitset<60001> bits, tmpBits;    int n = 0;    while( ++n ) {        bits.reset();        bits.set( 0 );        int tmp, total = 0;        for( int i = 1; i < 7; ++i ) {            cin >> tmp;            total += tmp * i;            for( int j = 0; j < 15; ++j ) {                if( tmp >= REGROUP[j] ) {                    bits |= ( tmpBits = bits << ( i << j ) );                    tmp -= REGROUP[j];                } else if( tmp != 0 ) {                    bits |= ( tmpBits = bits << ( tmp * i ) );                    break;                } else {                    break;                }            }        }        if( total == 0 ) {            break;        }        cout << "Collection #" << n << ":" << endl            << ( ( ( total & 1 ) == 0 ) && bits[total >> 1] ? "Can" : "Can't" )            << " be divided." << endl << endl;    }}

优化版代码如下:

#include <iostream>#include <vector>#include <bitset>#include <algorithm>using namespace std;bool divisionTest( vector<int>& marble, const int& half ) {    static bitset<60001> bits;    if( *( marble.end() - 1 ) > half ) {    // 剪枝: 若存在价值超过 half 的石头组则直接判为不可分        return false;    }    bits.reset();    bits.set( 0 );    int tmp, curMax = 0;    for( vector<int>::const_iterator pos = marble.begin(); pos < marble.end(); ++pos ) {        if( bits[half - *pos] ) {   // 剪枝: 若已达成条件则直接判为可分            return true;        }        for( int i = curMax; i >= 0; --i ) {            if( bits[i] && ( tmp = i + *pos ) <= half ) {                bits.set( tmp );            }        }        curMax = min<int>( curMax + *pos, half );   // 剪枝: 尽量降低 i 的上限    }    return bits[half];}int main() {    const int REGROUP[15] = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048,        4096, 8192, 16384 };    // 题设上限为 20000, 故最大值设为 2 的 14 次方    vector<int> marble;    int n = 0;    while( ++n ) {        marble.clear();        int tmp, total = 0;        for( int i = 1; i < 7; ++i ) {  // 1 ~ 6 共 6 种价值            cin >> tmp;            total += tmp * i;            for( int j = 0; j < 15; ++j ) { // 对每种价值的石头进行二进制组合                if( tmp >= REGROUP[j] ) {                    marble.push_back( i << j );                    tmp -= REGROUP[j];                } else if( tmp != 0 ) {                    marble.push_back( tmp * i );                    break;                } else {                    break;                }            }        }        if( total == 0 ) {            break;        }        sort( marble.begin(), marble.end() );   // 为剪枝做准备        cout << "Collection #" << n << ":" << endl;        if( ( ( total & 1 ) == 0 ) && divisionTest( marble, total >> 1 ) ) {            cout << "Can be divided." << endl << endl;        } else {            cout << "Can't be divided." << endl << endl;        }    }}
0 0
原创粉丝点击