例题7-7 UVA1354 Mobile Computing(50行AC代码)

紫书刷题进行中,题解系列【GitHub|CSDN

例题7-7 UVA1354 Mobile Computing(50行AC代码)

题目大意

给定房间宽度r和s个挂坠的重量,设计一个尽可能宽的且能挂上所有挂坠的天平(宽度<r)。

天平满足杠杆原理,假设左右臂长为a,b,左右挂坠重物为wl,wr,则a*wl=b*wr(仅一个挂坠时不用天平)

题目分析

一道技巧性极强的题目,需要深入思考

从示意图较容易想到用二叉树模型表示天平,其中每个挂坠均为叶子节点

若已知二叉树结构,那么很容易计算出最大宽度(木棒长度为1,左右挂坠重量已知,根据杠杆公式即可计算左右臂长),因此关键在于如何枚举二叉树

一分析,发现之前的排列和子集枚举思路都用不了了,这是一个树形结构,与之前的线性结构不同,因此需要转变思路。

最直接的想法(算法1):

  1. 每次任意选取两个点,构建成一颗二叉子树后重新将根节点加入原集合,并删除被选中的两点

  2. 重复步骤1直至集合中仅1个节点

思考起来很舒服,但实现起来问题重重

问题1:怎么高效维护变化的集合?
答案1:二进制表示集合

有一个技巧,可用二进制表示集合,从二进制右侧开始计算位置,假设位置i为1表示当前集合包含原集合中的第i个元素。如下例子,十进制数26的二进制表示为11010,表示当前集合包含原集合的第1,3,4位元素

26D = 1 1 0 1 0 --> 1表示取相应位置的元素,0则不取
	  4 3 2	1 0 --> 表示集合中的第几个元素

本质也是哈希函数的思想,将集合映射为一个整数表示,同时可以利用位运算&,|,^完成集合交,并,补给等运算。

当若是按照算法1,删除两个点后在集合中易实现,但假如新节点就不好处理,不变维护集合,仔细想一想,我们是根据二叉树的递归定义设计的自底向上构造算法,那么是否可以用自顶向下的思路呢?

如何单纯从集合的角度来看,将每个子树都用一个子集合表示,那么从根开始,就是全集A,从集合A中任意选择一个子集L作为左子树,那么补集R就是右子树,以此递归分解下去,到集合只有一个元素时,说明到达叶子节点,执行相应动作,返回。(分治法

这算法相对于算法1有个优势,在于它的集合均是确定的,新增和删除的情况,这样便可充分发挥二进制构造子集的优势

算法设计

如下定义树的结构,静态数组tree保存每个集合对应的所有左右子树合理的组合结果

struct Tree {
    double l=0, r=0; // 到根最远的左右距离
};
vector<Tree> tree[1<<maxn]; // tree[i]表示集合i对应的所有左右子树(i子集)组合的结果
bool vis[1<<maxn]; // 标记集合是否被访问的数组

有个很重要的技巧,**给定二进制表示的集合,怎么计算它所有的子集?**实现代码如下:

a=b; // 初始化
while (a >= 0) a=(a-1)&b; // 计算b表示的集合的所有子集

比如b=110,计算过程如下(十分巧妙,类似于列出1~b的所有值,与b相与是为了保证为0的位置依旧为0)

a=110

a=(110-1)&110=100
a=(100-1)&110=010
a=(010-1)&110=000

AC代码(C++11,二叉树枚举;分治法;二进制表示子集)

#include<bits/stdc++.h>
using namespace std;
const int maxn=6;
int T, s;
double r, w[maxn], sum[1<<maxn]; // sum[i]表示集合i的总重量
struct Tree {
    double l=0, r=0; // 到根最远的左右距离
};
vector<Tree> tree[1<<maxn]; // tree[i]表示集合i对应的所有左右子树(i子集)组合的结果
bool vis[1<<maxn]; // 标记集合是否被访问的数组
void dfs(int subset) { // 枚举子集subset所有可能的左右子树(子集)组合
    if (vis[subset]) return; // 剪枝,已访问过子集subset
    vis[subset] = true; // 标记访问
    if (((subset-1)&subset) == 0) tree[subset].push_back(Tree{}); // 叶子节点(仅一个元素)
    else { // 内部节点
        for (int left=(subset-1)&subset; left > 0; left = (left-1)&subset) { // 枚举subset的所有子集(除了空集,全集)
            int right = left ^ subset; // left的补集,作为右子树
            dfs(left); dfs(right); // 对左右子树递归计算
            double dl = sum[right] / sum[subset]; // 左右子树到根的横向长度
            double dr = sum[left] / sum[subset];
            for (auto tl : tree[left]) { // 遍历所有可能的左右子树组合,保留所有符合条件的点
                for (auto tr : tree[right]) {
                    Tree t;
                    t.l = max(tl.l + dl, tr.l - dr); // 找最大的的左右距离,考虑左右重叠
                    t.r = max(tr.r + dr, tl.r - dl);
                    if (t.l + t.r < r) tree[subset].push_back(t); // 当前组合小于房间宽度r 
                }
            }
        }
    }
}
int main() {
    scanf("%d", &T);
    while (T --) {
        scanf("%lf %d", &r, &s);
        for (int i=0; i < s; i ++) scanf("%lf", &w[i]);
        for (int i=0; i < (1<<s); i ++) { // 遍历所有子集,计算其总重量
            sum[i] = 0; tree[i].clear(); // 初始化
            for (int j=0; j < s; j ++) if (i & (1<<j)) sum[i] += w[j]; // w[j]属于集合i
        }
        int root = (1<<s)-1; // 全集(最后结果集节点)
        memset(vis, 0, sizeof(vis)); // 初始化访问数组
        dfs(root); // 枚举二叉树
        double ans=-1;
        for (int i=0; i < tree[root].size(); i ++) ans = max(tree[root][i].l+tree[root][i].r, ans); // 找全集对应的最大左右和值
        if (ans + 1 < 1e-5) printf("-1\n");
        else printf("%.16lf\n", ans);
    }
    return 0;
}
发布了128 篇原创文章 · 获赞 87 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_40738840/article/details/104507971
今日推荐