我需要一个提示才能开始这个编程难题

Posted

技术标签:

【中文标题】我需要一个提示才能开始这个编程难题【英文标题】:I need a hint to start on this programming puzzle 【发布时间】:2011-12-05 01:59:20 【问题描述】:

你有一个满屋子的天平和砝码。每个天平重 10 磅,当其左右两侧的重量总和完全相同时,被认为是完全平衡的。您在一些天平上放置了一些砝码,并且您在其他天平上放置了一些天平。给定天平的排列方式以及每个天平上有多少额外重量的描述,确定如何向天平添加重量以使它们完全平衡。

平衡一切的方法可能不止一种,但始终选择将额外重量放在最低平衡点上的方法。

输入文件将以单个整数 N 开头,指定有多少余额。 余额 0 由第 1 行和第 2 行指定,余额 1 由第 3 和第 4 行指定,等等... 每对行的格式如下:

WL <balances>
WR <balances>

WL 和 WR 分别表示添加到左侧和右侧的权重。是位于该天平那一侧的其他天平的空格分隔列表。可能包含零个或多个元素。

考虑以下输入:

4
0 1
0 2
0
0 3
3
0
0
0

Balance 0 has balance 1 on its left side and balance 2 on its right side
Balance 1 has balance 3 on its right side
Balance 2 has three pounds on its left side
Balance 3 has nothing on it

由于天平 3 上没有任何东西,它已经完全平衡,总共重 10 磅。 天平 2 上没有其他天平,所以我们需要做的就是通过在其右侧放三磅来平衡它。现在它总共重 16 磅。 天平 1 右侧有天平 3,重 10 磅,所以我们只在其左侧放 10 磅。天平 1 的总重量为 30 磅。 天平 0 左侧有天平 1(30 磅),右侧有天平 2(16 磅),我们可以通过在右侧增加 14 磅来平衡它。

输出应该是N行长,第n行列出了添加到第n个余额的权重,格式如下:

<index>: <weight added to left side> <weight added to right side>

所以这个问题的输出是:

0: 0 14
1: 10 0
2: 0 3
3: 0 0

我试过了,但我猜我的编程真的很糟糕。我应该从哪里开始?请不要发布解决方案;我想学习。

【问题讨论】:

@Adam:考虑将正在发生的事情建模为一个树结构,其中每个余额都是一个分支节点,每个权重都是一个叶子。 顺便说一句,我认为这是一个有趣的问题,应该重新提出 - 其他几个人似乎也同意我的观点。 我投票支持重新打开这个,这是一个非常有趣的问题 我想知道答案 这是一个很有趣的问题,我投票支持重新开放 【参考方案1】:

这是你的树

           (0)
     -------------      
     |           | 
    (2)         (1)
 ---------    -------   
 |       |    |     |
[3p]     ..   ..   (3)
                  ------
                  |     |
                 ...    ..

你的逻辑应该在内存中创建这棵树,每个节点都包含以下数据结构。

BalanceIdx: Integer;    //The balance index number if any. -1 indicates none
InitialPounds: Integer; //Initial weight in the node 
AddedPounds: Integer;   //The weight you added when processing. Default is 0
TotalWeight: Integer;   //**Total weight of a node including all its children. This is also our indication that a particular branch is already traversed. Default is -1

我们谈论的是一个递归函数,它基本上知道它在树的任何节点中只有两条路径或没有路径可循。每个递归都被认为是坐在天平的盘子上。

这是逻辑。

    从根开始,直到找到一个无路可走的节点。

    InitialPounds 的帮助下更新它的 TotalWeight

    现在看看节点的另一个兄弟节点是否有TotalWeightset。如果NO,将递归函数的根设置在那里并执行。

    如果YES,计算差值并更新您所在位置的AddedPounds。现在转到父级并更新其TotalWeight。 (不要忘记为余额添加 10 便士)。然后去祖父母那里重复3。

一旦递归函数完成对整个树的遍历,您就会在每个节点中记录AddedPounds。使用另一个递归函数来构造输出。

此答案适用于您要求的初学者。

【讨论】:

每个余额需要指向一个左孩子和一个右孩子。并且不需要存储其自己的 Idx。此外,您可能希望分别存储左右 AdditionalPounds,除非您想使用虚拟天平处理增加的重量。此外,InitialPounds 和 TotalWeight 可以是递归函数中的本地变量,而不是存储在数据结构中。此外,您没有在同一个平底锅上处理多个余额的规定,但我想您是故意遗漏的,因为 OP 只要求开始(4 年前:P) 每个递归都不被认为是在天平的根上——而是在天平的一个盘子上。 1.请注意我说过“我们正在谈论一个递归函数,它基本上知道它在树的任何节点中只有两条路径或没有路径可遵循”。 BalanceIdx 用作指示该“盘子”是否挂有儿童天平。 2. InitialPounds 和 TotalWeight 不能是局部变量,因为前者是输出的一部分,而后者是您将在最后累积的直接输出。感谢您的评论。 看看我的回答。我通过让我的递归函数返回该余额的权重(10lb)加上其平底锅上的所有东西(调整后)来汇总局部变量中子树的权重。使用递归函数可以让您以这种方式使用局部变量。如果您在没有递归的情况下对树进行迭代遍历,则无法做到这一点,但是递归为每个子树提供了一个单独的本地实例。无论如何,我猜你是说有一个单独的数据结构来表示平衡?喜欢struct balance struct pan left, right; ? BalanceIdx 索引一个数组?【参考方案2】:

剧透警告:包括完整的解决方案(读取输入除外)

这个问题很老了,而且看起来很有趣,所以我只是去解决了它(在 C 中,而不是像这样标记的 php),在几个类似提示的开头段落之后。我使用了详细的变量名称并进行了注释,因此它应该作为伪代码工作。我本来打算编写类似 C 的伪代码,但从未遇到任何值得总结的内容。


这个问题的主要复杂性是您不能使用二元树,因为单个天平可以在其一个或两个平底锅上具有多个其他天平。这里最简单的方法可能是节点的链表,其中每个节点都有左右子节点,还有一个兄弟指针。要让所有余额都位于余额的左侧,请遍历从节点的左子节点开始的兄弟指针链表以获取您正在查看的余额。

由于输入格式将数字分配给所有余额,因此最简单的方法是将这些索引用于结构数组。如果您可以假设余额少于 2^31,则可以通过使用 32 位整数而不是 64 位指针来节省内存。 (负索引是空的标记,就像基于指针的树/列表实现中的 NULL 指针)

struct balance 
    // weights sitting directly on the pans of this balance
    float weight_left, weight_right;  // or unsigned int; the question only said the balance-count was an int.  I assumed the balance-IDs were int, too

    // optionally: float adjustment;  // For recording our changes.  negative or positive for adding to left or right balance.

    // references to other balances: negative means empty-list, otherwise index into an array.
    int next;           // linked-list of sibling balances on the same pan of a parent
    int left, right;    // list-heads for balances on the left and right pan
;

当他们说你应该为“最低”天平增加重量时,我猜他们的意思是根在底部。它不是悬挂在某物上的天平树。

您可以为已携带天平的天平添加权重。因此,在树的叶子上增加空盘的重量并不复杂。 (这需要以一种保持每个子树独立平衡的方式来划分权重)。

所以这看起来很容易递归解决。

子树可以自己平衡,无需任何其他平衡的信息。 重量和其他天平的组合构成左右平底锅的负载无关紧要。你只需要这两个总数。通过直接向打火机平底锅添加重量来平衡。 平衡其所有子树之后平衡平衡。您可以在平衡子树的同时汇总它们的权重。

所以算法是:

static const float BALANCE_WEIGHT = 10.0f;

// return total weight of the balance and everything on it, including the 10.0f that this balance weighs
// modify the .weight_left or .weight_right of every node that needs it, in the subtrees of this node
float balance(struct balance storage[], int current)

    float lweight = 0, rweight = 0;

    // C++ can make this more readable:
    // struct balance &cur = storage[current];

    // loop over all the left and then right children, totalling them up
    // depth-first search, since we recurse/descend before doing anything else with the current
    for (int i = storage[current].left ; i >= 0 ; i = storage[i].next )
        lweight += balance(storage, i);

    for (int i = storage[current].right; i >= 0 ; i = storage[i].next )
        rweight += balance(storage, i);


    lweight += storage[current].weight_left;
    rweight += storage[current].weight_right;

    float correction = fabsf(rweight - lweight);

    // modify the data structure in-place with the correction.
    // TODO: print, or add to some other data structure to record the changes
    if (lweight < rweight) 
        storage[current].weight_left += correction;
     else 
        storage[current].weight_right += correction;
    

    return BALANCE_WEIGHT + rweight + lweight + correction;

要记录您所做的更改,请在数据结构中使用额外的字段,或者在您从深度优先平衡恢复时销毁原始数据(因为不再需要它)。例如如果左侧需要添加权重,则存储.weight_left = correction; .weight_right = 0;,否则执行相反的操作。


如果有一个全局数组,这个实现将使用更少的堆栈空间,而不是每次递归调用都必须传递storage 指针。这是一个额外的值,必须在寄存器调用 ABI 中保存/恢复,并直接占用堆栈调用 ABI(如 32 位 x86)中的额外空间。

涉及当前节点的weight_leftweight_right 的所有计算都发生在最后,而不是在开始时读取它们,然后必须重新读取它们以进行 += 读取-修改-写入。编译器无法进行这种优化,因为它不知道数据结构没有循环,导致balance(subtree) 修改其中一个权重。

由于某种原因,x86 gcc 5.2 -O3 compiles this to really huge code。 -O2 更理智。 clang 可以与 -O3 搭配使用,但缺少一些优化。

【讨论】:

请注意,这不是二叉树问题。由于提问者给出的(太)简单的例子,我首先错过了。【参考方案3】:

这个答案也会给出工作代码,这次是 PHP。

一些重要的观察:

即使给定的示例从不会在任何天平的任一侧放置一个以上的天平,也允许多个天平位于天平的一侧; 不能保证只有一个根余额,房间里可能有几个分离的堆栈; 输入格式允许一个天平放置在多个其他天平上,或者在同一天平一侧放置多次,甚至放置在其自身上。假设此类和其他循环输入将被视为无效; 条件 “选择在最低天平上放置额外重量的方式” 相当于说 “您只能在每个天平的一侧添加重量,不能同时添加"

定义了四个函数:

parseInput 接受输入字符串并返回以逻辑方式表示数据的对象数组; addWeights 获取余额的索引 (ID) 并计算应在其哪一侧添加权重,并将该权重存储在新属性中。该计算使用递归来计算位于该天平任一侧的所有天平的权重,在这些天平与添加的权重平衡之后。如果检测到循环,它会抛出错误,因此也可以防止无限递归; balance 访问所有余额,如果尚未平衡,则调用上述 addWeights。这将涵盖多个 root 平衡配置; outputString 采用完整的结构并以预期的输出格式返回一个字符串。

数据结构是对象对的数组。示例数据将转换为以下内容,并在计算过程中添加 addedWeight 属性:

[
  [
     'weight' => 0, 'balances' => [1], 'addedWeight' =>  0 ,
     'weight' => 0, 'balances' => [2], 'addedWeight' => 14 
  ], [
     'weight' => 0, 'balances' =>  [], 'addedWeight' => 10 ,
     'weight' => 0, 'balances' => [3], 'addedWeight' =>  0 
  ], [
     'weight' => 3, 'balances' =>  [], 'addedWeight' =>  0 ,
     'weight' => 0, 'balances' =>  [], 'addedWeight' =>  3 
  ], [
     'weight' => 0, 'balances' =>  [], 'addedWeight' =>  0 ,
     'weight' => 0, 'balances' =>  [], 'addedWeight' =>  0 
  ]
]

代码中的注释应该解释最重要的部分:

function parseInput($input) 
    $lines = preg_split('/(\r\n|\n)/', $input, null, PREG_SPLIT_NO_EMPTY);
    $count = (int) array_shift($lines); // we don't need this item
    $balances = array();
    $side = array();
    foreach($lines as $line) 
        // get the numbers from one line in an array of numbers
        $args = array_map('intval', explode(' ', $line));
        $obj = new stdClass();
        // first number represents the weight
        $obj->weight = array_shift($args);
        // all other numbers represent IDs of balances
        $obj->balances = $args;
        // collect this as a side, next iteration will fill other side
        $side[] = $obj;
        if (count($side) == 2) 
            // after each two lines, add the two objects to main array
            $balances[] = $side;
            $side = array();
        
    
    return $balances;


function addWeights(&$balances, $id) 
    if ($id == -1) 
        // There is no balance here: return 0 weight
        return 0;
    
    // Check that structure is not cyclic:
    if (isset($balances[$id][0]->addedWeight)) 
        throw new Exception("Invalid balance structure: #$id re-encountered");;
    
    $total = 10; // weight of balance itself
    $added = 0;
    foreach($balances[$id] as &$side) 
        // get the direct weight put in the balance: 
        $weight = $side->weight;
        // add to it the total weight of any balances on this side,
        // by using recursion
        foreach($side->balances as $balanceId) 
            $weight += addWeights($balances, $balanceId);
        
        // calculate difference in left/right total weight
        $added = $weight - $added;
        $total += $weight;
        // create new property with result
        $side->addedWeight = 0;
    
    // set either left or right added weight:
    $balances[$id][$added > 0 ? 0 : 1]->addedWeight = abs($added);
    $total += abs($added);
    return $total;


function balance(&$balances) 
    // If the only root balance was at index 0, we could just 
    // call addWeights($balances, 0), but it might be elsewhere
    // and there might even be multiple roots:
    foreach($balances as $index => $balance) 
        if (!isset($balance[0]->addedWeight)) 
            addWeights($balances, $index);
        
    


function outputString(&$balances) 
    $output = '';
    foreach($balances as $index => $balance) 
        $output .= "$index: $balance[0]->addedWeight $balance[1]->addedWeight\n";
    
    return $output;

它的使用方法如下:

// test data
$input =
"4
0 1
0 2
0
0 3
3
0
0
0";
// 1. transform input into structure
$balances = parseInput($input);
// 2. main algorithm
balance($balances);
// 3. output the result in desired format
echo outputString($balances);

输出是:

0: 0 14
1: 10 0
2: 0 3
3: 0 0

【讨论】:

很好地发现问题并没有说必须有一个根。我什至没有意识到我在做出这样的假设! 谢谢@PeterCordes,我又写了一些东西并添加了一个检查以防止遇到树循环。我们都知道这纯粹是为了好玩,因为这个问题可以追溯到 2011 年,但有趣的是做而不是奖励。 是的,让我的解决方案高效化让我很开心:表明与一些更笨拙的解决方案不同,您可以动态跟踪添加的质量,作为递归中的返回值/局部变量。您只需向数据结构中添加任何内容,即可以与遍历不同的顺序将其打印出来。尽管“满室余额”问题的多根部分可能需要对此进行修改。不过,如果我打扰了,请 IDK。 确实,动态总重量计算很好。多根支持很容易在您的代码中实现:您只需要跟踪访问过的节点(例如,作为结构中的附加布尔值)。然后,不要在 main 中使用索引 0 调用 balance 一次,而是遍历整个数组,并在尚未访问的任何元素上调用 balance (这些是额外的根)。可以通过维护未访问节点的双链表来优化,但我没有这样做。 很好的解决方案,我可以在 python 中找到解决方案。【参考方案4】:

概述

这个问题需要一个树结构来表示基于余额的余额。为了解决这个问题,你需要能够:-

    找到所有余额。这需要某种形式的递归函数,您可以在其中访问左右孩子。

    计算重量。从给定的天平中,找出其左侧和右侧的重量。

    添加权重。鉴于问题表明需要首先将权重添加到最低的余额,然后您反复查看树以查找没有子节点或已平衡子节点的余额。

【讨论】:

以上是关于我需要一个提示才能开始这个编程难题的主要内容,如果未能解决你的问题,请参考以下文章

这就是编程的终极难题? | 每日趣闻

C语言难题

C++ 并行编程中的“锁”难题

无点编程难题

巧用“搜索”解决自学编程遇到的难题

网络分流器-网络分流器-多核编程的几个难题及其应对策略