题意:给出房间宽度r和s个挂坠的重量wi。设计一个尽量宽(但宽度不能超过房间宽度r)的天平,挂着所有挂坠。天平由一些长度为1的木棍组成。木棍的每一端要么挂一个挂坠,要么挂另一个木棍,设n和m分别是两端挂的总重量,要让天平平衡,必须满足n*a=m*b。(0<r<10,1s6)。输出和标准答案的绝对误差不应该超过。
分析:好了数值很小,可以暴力,问题是怎么枚举二叉树。这是本题的难点,很多人第一次做这种类型的题目时,都不知道怎么下手,当然我也是,考虑很久,也写不出枚举二叉树算法,看过解析才知道原来可以这么简单,才知道自己对二叉树的理解还是欠缺了。二叉树的核心在于结点可以组成节点。
下面以一个例子来说明:
4个挂坠,每个挂坠重量分别为1,2,3,5。房间宽度为1.7143。
3 0.666|0.333 ------------------- | | 1 2
先将第一个节点和第二个节点组成一个节点,重量为3,左边宽度为0.6666,右边为0.3333,增加到节点列表中,节点列表为1,2,3,5,3。然后继续找下一层,找另外两个没找过的节点为3,5,组成节点,重量为8,增加到节点列表中,当前节点列表为1,2,3,5,3,8,访问列表为0,0,0,0,1,1。0表示访问过。所以继续下一层,然后就剩下组合节点3和8了,取3左边的长度为0.666,8右边的长度为0.375。相加为1.041再加上1就是2.041,超过房间宽度,那么这种情况就不符合,继续循环这次是8和3,取8的左边为0.625取3的右边0.3,还是超过,不符合,没有可访问节点,回溯。回溯到第二层,目前循环到刚刚是3和5,5访问过不符合,下面就继续访问节点4也就是重量3,是组合节点有其左右长度,分别为0.666和0.333,一开始是3和3取也就是节点2和节点4,取2节点左边长度为0,4节点右边长度为0.333,总长度为1.333,将节点6重量为6添加到节点列表中,节点列表为1,2,3,5,3,6,节点6左右长度分别是0.5和0.8333。可以进行下一层,访问列表为0,0,0,1,0,1。可访问的节点是3和6,然后取3的左边和6的右边,不符合,循环,取6的左边和3的右边为0.5+1<1.7041,符合条件,达到顶层,退出和当前最大比较。回溯下一个。以此类推可以访问过所有节点组成的所有二叉树。
代码部分:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include<iostream>
using namespace std;
const int maxn = 20;
double lw[maxn], rw[maxn], r; int w[maxn], vis[maxn]; int indexw = 0; int n; double ans;
void dfs(int layer) {
if (layer == n)return;
for (int i = 0; i < maxn; i++) {
if (vis[i]) {
for (int j = 0; j < maxn; j++) {
if (i != j && vis[j]) {
double L = max(lw[i], lw[j] - 1); double R = max(rw[j], rw[i] - 1);//防止右边的节点的左长度超过左边节点的左长度,同理右也是
if (L + R + 1 < r) {//判断是否小于房间长度
if (layer == n - 1)ans = max(ans, L + R + 1);
vis[i] = vis[j] = 0;//置为访问过
int id = indexw++;
vis[id] = 1;//新节点置为可访问
w[id] = w[i] + w[j];//新节点重量为两节点之和
lw[id] = w[j]*1.0 / w[id] + L;//不能忘记加上之前的右边长度
rw[id] = w[i]*1.0 / w[id] + R;
dfs(layer + 1);
vis[i] = vis[j] = 1;
vis[--indexw] = 0;//新节点要置为不可访问!
}
}
}
}
}
}
int main() {
int kase;
cin >> kase;
while (kase-- > 0) {
indexw = 0; ans = -1;
memset(vis, 0, sizeof(w));
memset(lw, 0, sizeof(lw));
memset(rw, 0, sizeof(rw));
cin >>r>> n;
for (int i = 0; i < n; i++) { cin >> w[i]; vis[indexw++] = 1; }
if (n == 1) { printf("0.0000000000\n"); continue; }
dfs(1);
printf("%.10lf\n", ans);
}
return 0;
}
老师的代码是玄学(表示学不来)
// UVa1354 Mobile Computing
// Rujia Liu
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
struct Tree {
double L, R; // distance from the root to the leftmost/rightmost point
Tree():L(0),R(0) {}
};
const int maxn = 6;
int n, vis[1<<maxn];
double r, w[maxn], sum[1<<maxn];
vector<Tree> tree[1<<maxn];
void dfs(int subset) {
if(vis[subset]) return;
vis[subset] = true;
bool have_children = false;
for(int left = (subset-1)⊂ left; left = (left-1)&subset) {
have_children = true;
int right = subset^left;
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++) {
Tree 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);
}
}
if(!have_children) tree[subset].push_back(Tree());
}
int main() {
int T;
scanf("%d", &T);
while(T--) {
scanf("%lf%d", &r, &n);
for(int i = 0; i < n; i++) scanf("%lf", &w[i]);
for(int i = 0; i < (1<<n); i++) {
sum[i] = 0;
tree[i].clear();
for(int j = 0; j < n; j++)
if(i & (1<<j)) sum[i] += w[j];
}
int root = (1<<n)-1;
memset(vis, 0, sizeof(vis));
dfs(root);
double ans = -1;
for(int i = 0; i < tree[root].size(); i++)
ans = max(ans, tree[root][i].L + tree[root][i].R);
printf("%.10lf\n", ans);
}
return 0;
}
他用的是二进制枚举子集,每次都取出左右子集,枚举集合的所有子集,最后进行子集判断,果然厉害。