POJ 1014: Dividing

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

错误解法 1: 依照价值从大到小的顺序尝试凑数 (不回溯).

因为裁判的測试数据不够全面, 基于这样的思路设计出的程序在提交之后是可以 AC 的. 然而有一个典型的反例: “0 0 3 0 3 1”, 这是一个可分的组合, 但用上述思路则会判定为不可分.

其实, 对于绝大多数编程者来说, 这样的思路在通常应该会被第一时间在头脑中否决. 然而一些编程者在对 DFS 的尝试中逻辑出现了错误, 导致程序实质上实现的正是上述思路. 因为可以 AC, 所以编程者会误觉得自己编写出了正确的 DFS 版程序. 以下提供原文链接, 供有兴趣自行研究的读者參考.

错误解法 2: 循环尝试最大化选取单种价值的石头 (有回溯).

具个详细的样例: 首先尝试以 half 价值为限, 最大化选取价值为 6 的石头 (全选或选择 int( half / 6 ) 块, 设为 n6) 再以 half - n6 价值为限, 最大化选取价值为 5 的石头, 以此类推. 若发现无法凑出 half 价值, 则回溯, 尝试以 half 价值为限, 最大化选取价值为 5 的石头, 以此类推. 编程者觉得这样的方法是 “扩展的找零问题解法”.

其实, 这样的解法相同是错误的, 原因在于这样的解法从根本上讲是基于一个命题, 即 “对于随意一个价值限度, 假设可以凑得出, 则必然存在一种最大化选取某一种价值的石头的凑法”. 此命题的反例非常easy找到: “0 0 0 2 2 2”. 但是基于上述思路编写的程序提交后仍能 AC, 所以重新使编程者误觉得自己的思路是正确的. 以下提供原文链接, 供有兴趣自行研究的读者參考.

解题要点 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,n?2k

则 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;
        }
    }
}
时间: 2024-10-19 02:52:42

POJ 1014: Dividing的相关文章

POJ 1014 Dividing 背包

这道题使用多重背包,不过其实我也不太明白为什么叫这个名字. 因为感觉不是什么多重,而是物体的分解问题. 就是比如一个物体有数量限制,比如是13,那么就需要把这个物体分解为1, 2, 4, 6 如果这个物体有数量为25,那么就分解为1, 2, 4, 8, 10 看出规律吗,就是分成2的倍数加上位数,比如6 = 13 - 1 - 2 - 4, 10 = 25 - 1 - 2 - 4 - 8,呵呵,为什么这么分解? 因为这样分解之后就可以组合成所有1到13的数,为25的时候可以组合成所有1到25的数啦

POJ 1014 Dividing (多重背包)

Dividing Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 58921   Accepted: 15200 Description Marsha and Bill own a collection of marbles. They want to split the collection among themselves so that both receive an equal share of the marbl

POJ 1014 Dividing(多重背包+二进制优化)

http://poj.org/problem?id=1014 题意:6个物品,每个物品都有其价值和数量,判断是否能价值平分. 思路: 多重背包.利用二进制来转化成0-1背包求解. 1 #include<iostream> 2 #include<string> 3 #include<cstring> 4 #include<cstdio> 5 #include<algorithm> 6 using namespace std; 7 8 const i

POJ 1014 Dividing 【DP 之 多重背包 / 二进制优化】

Language: Default Dividing Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 63647   Accepted: 16488 Description Marsha and Bill own a collection of marbles. They want to split the collection among themselves so that both receive an equal

多重背包,附上例题(POJ - 1014 Dividing)

直接上例题 Dividing Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 72067   Accepted: 18813 Description Marsha and Bill own a collection of marbles. They want to split the collection among themselves so that both receive an equal share of the

POJ 1014 Dividing(多重背包)

Dividing Description Marsha and Bill own a collection of marbles. They want to split the collection among themselves so that both receive an equal share of the marbles. This would be easy if all the marbles had the same value, because then they could

POJ 1014 Dividing【多重背包+二进制优化】

大意: 价值1, 2, 3, ……, 6的物品分别a1, a2, ……, a5, a6件 问能否把这些物品分成两份,使其具有相同的价值(所有物品必须全部用上) 分析: 给个物品有多件,即多重背包 只要看能不能将这些物品拼成   总价值 的 一半就可以了 转化为01背包是用二进制优化,否则会超时 代码: 1 #include <iostream> 2 #include <cstring> 3 #include <cstdio> 4 using namespace std;

证明 poj 1014 取模优化剪枝,部分递归 存在错误

做了这道题目,我才发现一道题即便可行,但是正确性也不一定.多数由于题目数据太弱. poj 1014 Dividing 题目大意:有6堆石头,权重分别为1 2 3 4 5 6,要求输入 每堆个数 ,求是否可以平分石头使得两堆价值相同. 网上对这道题的做法就两种,其中有错误的版本,却也可以AC.起初这让我等菜鸟感慨代码的简洁,但无法得出正确性的证明 接下来就对两种方法的错误性进行证明. 1.多重背包 #include <map> #include<string> #include &l

证明 poj 1014 模优化修剪,部分递归 有错误

这个问题是存在做.我发现即使是可行的一个问题,但不一定正确. 大部分数据疲软,因为主题. id=1014">poj 1014 Dividing 题目大意:有6堆石头,权重分别为1 2 3 4 5 6,要求输入 每堆个数 ,求能否够平分石头使得两堆价值同样. 网上对这道题的做法就两种,当中有错误的版本号.却也能够AC. 起初这让我等菜鸟感慨代码的简洁,但无法得出正确性的证明 接下来就对两种方法的错误性进行证明. 1.多重背包 #include <map> #include<