标签:
写在前面: 本题非常有意思, 因此我在编程前后和撰写本文前, 在网上查询并阅读了大量关于本题的解题报告. 当中有两种似是而非但提交后却可以 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 才是这道题的唯一 (严谨点说的话是 “非常可能是唯一”) 可行算法. 大致流程例如以下:
解题要点 2: 对每种价值的石头进行二进制组合.
一个广为人知的小原理:
对于任一满足
2k<n<2k+1 的自然数 n, 将其拆分成下列数列之和:20,21,...,2k,n?2k
则 1 ~ n 中的任一自然数都可以用上述数列中某些项之和来表示.
每种价值的石头原本是以单体的形式存在的. 利用上述原理, 就可以使每种价值的石头以 “装箱” 的形式存在, 同一时候保证这些 “石头箱” 所可以生成的价值组合全然等同于原本的石头单体所可以生成的价值组合. 如此一来, 解题要点 1 中算法流程第 2 步的第一重循环的次数便会大幅降低.
解题要点 3: 剪枝.
仅依据前面介绍的两个解题要点来编程, 就已可以达到相当快的执行速度 (请參考 “极简版代码”). 然而实际上还是可以依据题目的特点在前两个解题要点的基础上做进一步的优化. 详细例如以下:
经过上述优化, 提交后程序的执行时间少于 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;
}
}
}
标签:
原文地址:http://www.cnblogs.com/mengfanrong/p/5240240.html