版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原题连接:UVA 1354
题目大意
题目分析
可以把挂坠和横放的木棍都看成结点,则整个天平就是一个二叉树,且每个结点要么是叶子结点要么是有2个孩子的内部结点,例如上图中的3种天平就对应于下图3个二叉树:
而且不同的天平之间可以重叠。同时,对于一棵确定的二叉树,可以计算出每个天平的确切位置,进而计算出整个天平的宽度。所以,本题的核心是:如何枚举出所有需要的二叉树
。下面介绍两种方法:
自底向上枚举
因为二叉树有s
个叶子结点,每个内部结点2个孩子,所以一共有s-1
个内部结点,即总共2s-1
个结点。
所以我们可以:
- 将初始的s个挂坠看成s个子树
- 每次选择两个挂坠形成新的子树加入,然后递归
s-1
次,则一共形成了2s-1
个结点 - 注意每次形成新子树的时候,都需要判断一下宽度是否溢出,若是则剪枝
- 例如以4个挂坠
{1,1,2,3}
为例,下面画出解答树的一部分:(每个结点所能形成的所有子树并未完全画出)
#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
// 自下而上的递归
const int maxs = 6; // 最多的挂坠数
const double EPS = 1E-9; // 最大误差精度
struct node {
double w; // 重量
double left, right; // 左右子树的最大宽度
node() :left(0.0), right(0.0), w(0) {}
}Node[2 * maxs]; // s个叶子结点和s-1个内部结点,一共2s-1个结点
int vis[2 * maxs];
double r;
int s; // 宽度和挂坠数目
double maxr; // 求得的最大宽度
int isRight(node n) {
// 检验宽度是否合格
double width = n.left + n.right;
if (width <= r + EPS) return 1;
else return 0;
}
void init() {
maxr = -1;
memset(vis, 0, sizeof(vis));
cin >> r >> s;
for (int i = 0; i < s; i++) {
cin >> Node[i].w;
Node[i].left = Node[i].right = 0;
}
}
void dfs(int index) {
// 构造第index个结点 0-2s-2
if (index == 2 * s - 1) {
// 已经构造了 2s - 1 个结点
if (Node[index - 1].left + Node[index - 1].right > maxr)
maxr = Node[index - 1].left + Node[index - 1].right;
return;
}
// 否则从 0 到 index - 1 中找节点
for (int i = 0; i < index; i++) {
if (vis[i]) continue;
vis[i] = 1;
for (int j = 0; j < index; j++) {
if (vis[j]) continue;
vis[j] = 1;
Node[index].w = Node[i].w + Node[j].w;
// i 做左子树,j做右子树
double left = Node[j].w / Node[index].w;
double right = Node[i].w / Node[index].w;
Node[index].left = max(left + Node[i].left, Node[j].left - right);
Node[index].right = max(Node[i].right - left, right + Node[j].right);
if (isRight(Node[index])) dfs(index + 1);
vis[j] = 0; // 恢复原状
}// for
vis[i] = 0; // 恢复原状
}// for
}
int main() {
int t;
cin >> t;
while (t--) {
init();
if (s == 1)
printf("%.10lf\n", 0.0); // 特例
else {
dfs(s);
if (maxr == -1) printf("-1\n");
else printf("%.16lf\n", maxr);
}
}
return 0;
}
自顶向下枚举(记忆化搜索)
上述的枚举方法有一定的优化空间,例如有些树结点被枚举了多次(上图种画黑框的部分)。所以,我们可以使用 自顶向下 的方式构造,每次枚举左右子树需要用到的子集。
- 之前也总结过类似的 子集生成博客,这里我们使用的是二进制表示法,使用一个二进制数来表示子集的状态,即例如有s个元素,则我们用s个二进制数来表示一个子集,当第k位为1时表示第k个元素包含在子集中;
- 这样我们同时还能用一个数组
vis[1 << s]
来表示该子集是否被枚举过了
例如我们有三个元素{1,2,3}
,则一共有8个子集,用二进制表示为
111
110
101
100
011
010
001
000
有s个元素的子集的二进制生成如下:
for (int i = 0; i < (1 << s); i++) {
i 为 000 到 111
}
for (int i = (1 << s) - 1; i >= 0; i--) {
i 为 111 到 000
}
下面我们讨论如何枚举当前子集的所可能生成的二叉树的所有情况:
- 显然,二叉树的终止状态应该是该子集只包含一个元素,也就是该二进制数
只包含一个1
- 假设该二进制数包含不止一个1,则我们可以继续拆分该子集
例如如果一个子集表示为1101
,即其包含了{1,3,4}
三个位置的元素,其可以继续拆分,例如左子树含有第{1,3}
个元素,即0101
,则右子树就含有第4
个元素,即1000
,而且这样的操作可以用异或来表示,即1101 = 0101 ^ 1000
。则拆分1101
有以下这几种情况:(标红表示是叶子结点)
- 而且我们还注意到,一个子集的子集的二进制数肯定比它本身的二进制数要小,因为包含的1的个数要小,则划分子树的操作可以这样进行:
void split(int subset) {
// 划分子集
for (int left = (subset - 1) & subset; left; left = (left - 1)&subset) {
// & 表示清除掉原来集合中不为1的位置
// 例如 1101 集合下 1001 和 1011 实际上是一样的(因为第2个位置不可能是1),
// 即 & 的结果应该一样
int right = left ^ subset; // 异或,表示右子树的子集
}
}
- 同时对于每个结点(表示一个子集的状态),我们需要存储其能枚举的所有二叉树,用一个数组。
/* Mobile Computing */
// 困难的天平 二进制子集筛选
// 自上而下构造
#include<cstdio>
#include<cmath>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxr = 6;
struct node{
double l,r; // 左右子树的最远距离
node():l(0),r(0) {}
};
int vis[1 << maxr]; // 用来表示当前子集是否枚举过了
double sum[1 << maxr]; // 用来计算子集包含挂坠的重量
double w[maxr]; // 存储初始挂坠的重量
vector<node> Tree[1 << maxr]; // 来存储该子集可能的二叉树
double r; int s;
void init() {
cin >> r >> s;
for (int i = 0; i < s; i++) cin >> w[i];
// 计算子集的重量,当然也可以在dfs的时候计算
for (int i = 0; i < (1 << s); i++) {
vis[i] = 0;
sum[i] = 0;
Tree[i].clear();
for (int j = 0; j < s; j++) {
if (i & (1 << j)) sum[i] += w[j]; // 该子集包含j这个元素
}
}
}
void dfs(int subset) {
// 遍历subset这个子集,枚举其可能的二叉树
if (vis[subset]) return;
vis[subset] = 1;
bool hasechild = false; // 是否是叶子结点
for (int left = (subset - 1) & subset; left; left = (left - 1)&subset) {
hasechild = true;
int right = left ^ subset; // 异或,表示右子树的子集
double d1 = sum[right] / sum[subset]; // 左子树长度
double d2 = sum[left] / sum[subset]; // 右子树长度
dfs(left); dfs(right);
// 从叶子结点回溯返回
for (int i = 0; i < Tree[left].size(); i++) {
for (int j = 0; j < Tree[right].size(); j++) {
// 遍历所有左右子树的孩子情况,可以重叠
node t;
t.l = max(Tree[left][i].l + d1, Tree[right][j].l - d2);
t.r = max(Tree[right][j].r + d2, Tree[left][i].r - d1);
if (t.l + t.r <= r) Tree[subset].push_back(t);
}
}
}// for
// 如果是叶子结点,则只包含一个1,没有孩子
if (!hasechild)
Tree[subset].push_back(node());
}
int main() {
int t; cin >> t;
while (t--) {
init();
if (s == 1)
printf("%.16lf\n", 0.0);
else {
int root = (1 << s) - 1; //根结点
dfs(root);
double ans = -1;
for (int i = 0; i < Tree[root].size(); i++) {
if (Tree[root][i].l + Tree[root][i].r > ans)
ans = Tree[root][i].l + Tree[root][i].r;
}// for
printf("%.16lf\n", ans);
}
}
return 0;
}
摘自《算法竞赛入门经典 第二版 》 第 7.4 节