天平称重
【题目】用天平称重时,砝码可以放在左右两个盘中。如果只有1,3,9三个砝码,可以称出13以内的所有整数重量。如果有 1,3,9,27 四个砝码,可以把称重范围扩大到40。
如果只提供这种3倍关系的砝码序列。对任意给定的整数重量 x,求称重方案。
例如: 19 = 27 - 9 + 1
这种问题,如果仔细分析数学规律,当然可以让程序少走弯路。
简单的试算就能发现:当数字 n 在 a,b 两个相邻砝码中间时,到底是表示为 a + …, 还是 b - … 的形式决定于: n < b / 2
如果 n 足够小,就表示为 a + … ,否则就是 b - …
下面的解法,正数负数统一考虑了,有点绕。
counterWeight :: Int -> [Int]
counterWeight x
| x == near = [near]
| abs x <= abs (near `quot` 2) = near `div` 3 : counterWeight (x - near `div` 3)
| otherwise = near : counterWeight (x - near)
where
near = signum x * head (dropWhile (< abs x) ma)
ma = iterate (*3) 1
main :: IO ()
main = do
print $ counterWeight 4
print $ counterWeight 19
print $ counterWeight 14
print $ counterWeight 23
这里边有个陷阱要注意。
判定near一半的时候,要用 quot
,而不能用 div
。因为 div 是向负无穷方向取整,而quot是向零点方向取整。
如果观察不出来这种2分关系也不要紧。可以做如下考虑:
砝码的系列:
1 3 9 27 81 …
它前n项和:
1 4 13 40 121 …
显然,这个和表示前n项所能表示出的最大数字。
比如 10 , 在 13 之内,意味着前3项可表示,最大项9比10小,可以用 9 + …
比如 15, 在 40 之内, 意味着前4项可表示,最大项27比15大,必然是 27 - … 的形式
这样,就可以递归后再符号反转。这就不用考虑负数。
counterWeight :: Int -> [Int]
counterWeight n
| n == near = [near]
| n > near = near : counterWeight (n - near)
| otherwise = near : map (*(-1)) (counterWeight (near - n))
where
ma = iterate (*3) 1
he = scanl1 (+) ma
near = fst $ head $ dropWhile ((< n) . snd) (zip ma he)