问题描述
我是 Haskell 的新手,并且一直在通过做一些简单的编程挑战来练习。过去 2 天,我一直在尝试实现 the unbounded knapsack problem here. 我使用的算法描述为 on the wikipedia page,但对于这个问题,“重量”一词被“长度”一词取代。无论如何,我开始编写没有记忆的代码:
maxValue :: [(Int,Int)] -> Int -> Int
maxValue [] len = 0
maxValue ((l,val): other) len =
if l > len then
skipValue
else
max skipValue takeValue
where skipValue = maxValue other len
takeValue = (val + maxValue ([(l,val)] ++ other) (len - l)
我曾希望 haskell 会很好,并且有一些不错的语法(如 #pragma memoize
)来帮助我,但四处寻找示例,解决方案是用 this fibonacci problem code 解释的。
memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!)
where fib 0 = 0
fib 1 = 1
fib n = memoized_fib (n-2) + memoized_fib (n-1)
在掌握了这个例子背后的概念后,我感到非常失望 - 所使用的方法是超级hacky,只有在 1) 函数的输入是单个整数,并且 2) 函数需要递归计算值顺序 f(0),f(1),f(2),...
但是如果我的参数是向量或集合呢? 如果我想记住像 (其他人指出这个说法是错误的)f(n) = f(n/2) + f(n/3)
这样的函数,我需要为所有小于 n 的 i 计算 f(i)
的值,而我不需要这些值中的大部分。
我尝试通过传递一个我们慢慢填充为额外参数的备忘录表来实现我想要的:
maxValue :: (Map.Map (Int,Int) Int) -> [(Int,Int)] -> Int -> (Map.Map (Int,Int) Int,Int)
maxValue m [] len = (m,0)
maxValue m ((l,val) : other) len =
if l > len then
(mapWithSkip,skipValue)
else
(mapUnion,max skipValue (takeValue+val))
where (skipMap,skipValue) = maxValue m other len
mapWithSkip = Map.insertWith' max (1 + length other,len) skipValue skipMap
(takeMap,takeValue) = maxValue m ([(l,val)] ++ other) (len - l)
mapWithTake = Map.insertWith' max (1 + length other,len) (takeValue+val) mapWithSkip
mapUnion = Map.union mapWithSkip mapWithTake
但这太慢了,我相信是因为 Map.union takes too long,it's O(n+m)
而不是 O(min(n,m))
。此外,对于像 memoizaton 这样简单的东西,这段代码似乎很混乱。对于这个特定问题,您可能可以将 hacky 方法推广到 2 维,并进行一些额外的计算,但我想知道如何在更一般的意义上进行记忆。如何在保持与命令式语言中代码相同的复杂性的同时,以这种更通用的形式实现记忆化?
解决方法
如果我想记住像 f(n) = f(n/2) + f(n/3) 这样的函数,我需要为所有小于 n 的 i 计算 f(i) 的值,当我不需要这些值中的大部分。
不,懒惰意味着不使用的值永远不会被计算。您为它们分配一个 thunk 以防它们被使用过,因此它是非零数量的 CPU 和 RAM 专用于这个未使用的值,但例如评估 f 6
永远不会导致评估 f 5
。因此,假设计算一个项目的费用远高于分配一个 cons 单元格的费用,并且您最终会查看总可能值的很大一部分,那么此方法使用的浪费的工作量很小。
但是如果我的参数是向量或集合怎么办?
使用相同的技术,但使用与列表不同的数据结构。映射是最通用的方法,前提是您的键是 Ord,并且您可以枚举您需要查找的所有键。
如果您无法枚举所有键,或者您计划查找的键比可能的总数少很多,那么您可以使用 State(或 ST)来模拟在调用之间共享可写记忆缓存的命令式过程你的功能。
我很想向您展示这是如何工作的,但我发现您的问题陈述/链接令人困惑。您链接到的练习似乎等同于您链接到的维基百科文章中的 UKP,但我在该文章中没有看到任何与您的实现相似的内容。维基百科给出的“动态编程高级算法”明确设计为具有与您给出的 fib
记忆示例完全相同的属性。键是单个 Int,数组是从左到右构建的:从 len=0
开始作为基本情况,所有其他计算都基于已计算的值。此外,出于某种我不明白的原因,它似乎假设每个合法大小的对象至少有 1 个副本,而不是至少 0 个;但如果您有不同的限制,这很容易解决。
你实现的完全不同,从总 len 开始,并为每 (length,value)
步选择切割多少个大小为 length
的块,然后使用较小的 len 递归并删除您的权重值列表中的最前面的项目。它更接近于传统的“给定这些面额,您可以通过多少方式更改一定数量的货币”问题。这也适用于与 fib
相同的从左到右的记忆方法,但在两个维度上(一个维度是要更改的货币数量,另一个维度是剩余要使用的面额数量) .
我在 Haskell 中进行记忆的首选方法通常是 MemoTrie。它非常简单、纯粹,而且通常可以满足我的需求。
不用想太多,你就可以生产:
<html>
<head>
</head>
<style>
body{
background: #1f1f1f;
}
canvas{
}
</style>
<body >
<canvas height="1080px" width="1100px" id="graph">
</canvas>
</body>
<script>
/*Random Line Render Script aka Mini Browser Crasher */
/*XD Can't Revise This Script. Rofl Drains Memory. May crash low-end pc :V */
var c = document.getElementById("graph");
var dimension = [document.documentElement.clientWidth,document.documentElement.clientHeight];
c.width = dimension[0];
var ctx = c.getContext("2d");
var posx = [100,200,150,100,0];
var posy = [100,300,-100];
var posx2 = [600,400,600];
var posy2 = [500,500,500];
var posx3 = [];
var posy3 = [];
/*Generate random values for array( random starting point ) */
for(var i=0; i<2;i++){
posx2.push(500+Math.round(Math.random()*700));
posy2.push(Math.round(Math.random()*900));
}
for(var i=0; i<5;i++){
posx3.push(1000+Math.round(Math.random()*300));
posy3.push(0+Math.round(Math.random()*1000));
}
var posx_len = posx.length;
var posx2_len = posx2.length;
var posx3_len = posx3.length;
var xa,ya;
var opa =1;
var amount = 0.01;
var sinang = 0;
var distance1 = 0;
var distance2 = 0;
var t1,t2;
document.body.addEventListener('mousemove',(function (event) {
xa = event.clientX;
ya = event.clientY;
}));
/*Render Lines */
function draw(){
t1 =performance.now();
ctx.clearRect(0,1920,1080);
ctx.beginPath();
ctx.moveTo(posx[0],posy[0]);
for(var i= 0; i<posx_len;i++){
ctx.lineTo(posx[i],posy[i]);
ctx.arc(posx[i],posy[i],5,2 * Math.PI,false);
}
if(opa>1){
amount = -0.01*Math.random();
}
if(opa<0){
amount =0.01*Math.random();
}
opa =opa +amount;
ctx.moveTo(posx2[0],posy2[0]);
for(var i = 0; i<posx2_len;i++){
ctx.lineTo(posx2[i],posy2[i]);
ctx.arc(posx2[i],posy2[i],false);
}
ctx.moveTo(posx3[0],posy3[0]);
for(var i = 0; i<posx3_len;i++){
ctx.lineTo(posx3[i],posy3[i]);
ctx.arc(posx3[i],posy3[i],false);
}
sinang = sinang+0.01;
/*Frame Render Ends here*/
/*Calculation for next frame*/
for(var i = 0;i<posx_len;i++){
posx[i] = posx[i]+ (Math.cos(sinang)*i)/2;/* Sin curve for smooth value transition. Smooth assss Butter */
posy[i] = posy[i]+ (Math.cos(sinang)*i)/2;
/* Can't believe Distance Formula is useful ahaha */
if(Math.abs(posx[i]-xa)<500 && Math.abs(posy[i]-ya)<500){
ctx.moveTo(posx[i],posy[i]);
ctx.lineTo(xa,ya);
}
for(var j = 0;j<posx2_len;j++){
if(Math.abs(posx[i]-posx2[j])<500 && Math.abs(posy[i]-posy2[j])<500){
ctx.moveTo(posx[i],posy[i]);
ctx.lineTo(posx2[j],posy2[j]);
}
}
for(var j = 0;j<posx3_len;j++){
if(Math.abs(posx[i]-posx3[j])<500 && Math.abs(posy[i]-posy3[j])<500){
ctx.moveTo(posx[i],posy[i]);
ctx.lineTo(posx3[j],posy3[j]);
}
}
}
posx[posx.length-1]=posx[0];
posy[posy.length-1] = posy[0];
/*Repeat Above Steps. Should have done this in Multi-dimensional array. Ugh I feel sad now*/
for(var i = 0;i<posx2_len;i++){
posx2[i] = posx2[i]+ (Math.sin(sinang)*i)/2;
posy2[i] = posy2[i]-(Math.sin(sinang)*i)/2;
if(Math.abs(posx2[i]-xa)<500 && Math.abs(posy2[i]-ya)<500){
ctx.moveTo(posx2[i],posy2[i]);
ctx.lineTo(xa,ya);
}
for(var j = 0;j<posx3_len;j++){
if(Math.abs(posx2[i]-posx3[j])<500 && Math.abs(posy2[i]-posy3[j])<500){
ctx.moveTo(posx2[i],posy2[i]);
ctx.lineTo(posx3[j],posy3[j]);
}
}
}
posx2[posx2.length-1]=posx2[0];
posy2[posy2.length-1] = posy2[0];
for(var i = 0;i<posx3_len;i++){
posx3[i] = posx3[i]- (Math.sin(sinang)*i)/1.2;
posy3[i] = posy3[i]-(Math.sin(sinang)*i)/1.2;
if(Math.abs(posx3[i]-xa)<500 && Math.abs(posy3[i]-ya)<500){
ctx.moveTo(posx3[i],posy3[i]);
ctx.lineTo(xa,ya);
}
}
posx3[posx3.length-1]=posx3[0];
posy3[posy3.length-1] = posy3[0];
ctx.restore();
ctx.strokeStyle = 'rgba(255,255,'+opa+')';
ctx.stroke();
window.requestAnimationFrame(draw);
t2=performance.now();
console.log(t2-t1);
}
window.requestAnimationFrame(draw);
</script>
</html>
我没有你的输入,所以我不知道这会多快 - 记住 import Data.MemoTrie (memo2)
maxValue :: [(Int,Int)] -> Int -> Int
maxValue = memo2 go
where
go [] len = 0
go lst@((l,val):other) len =
if l > len then skipValue else max skipValue takeValue
where
skipValue = maxValue other len
takeValue = val + maxValue lst (len - l)
输入有点奇怪。我认为您也认识到这一点,因为在您自己的尝试中,您实际上记住了列表的长度,而不是列表本身。如果你想这样做,把你的列表转换成一个恒定时间查找数组然后记忆是有意义的。这是我想出的:
[(Int,Int)]
,
一般来说,Haskell 中的普通记忆化可以像在其他语言中一样实现,通过在缓存值的可变映射上关闭函数的记忆化版本。如果您想要像纯函数一样方便地运行该函数,则需要在 IO 中维护状态并使用 unsafePerformIO
。
对于大多数代码提交网站,以下备忘录可能就足够了,因为它仅取决于通常应该可用的 System.IO.Unsafe
、Data.IORef
和 Data.Map.Strict
。
import qualified Data.Map.Strict as Map
import System.IO.Unsafe
import Data.IORef
memo :: (Ord k) => (k -> v) -> (k -> v)
memo f = unsafePerformIO $ do
m <- newIORef Map.empty
return $ \k -> unsafePerformIO $ do
mv <- Map.lookup k <$> readIORef m
case mv of
Just v -> return v
Nothing -> do
let v = f k
v `seq` modifyIORef' m $ Map.insert k v
return v
从你的问题和评论来看,你似乎是那种永远失望的人(!),所以使用 unsafePerformIO
可能会让你失望,但如果 GHC 真的提供了备忘录 pragma,这可能是它会在幕后做什么。
举个简单的例子:
fib :: Int -> Int
fib = memo fib'
where fib' 0 = 0
fib' 1 = 1
fib' n = fib (n-1) + fib (n-2)
main = do
print $ fib 100000
或者更重要的是(剧透?!),你的maxValue
的一个版本只记住了长度:
maxValue :: [(Int,Int)] -> Int -> Int
maxValue values = go
where go = memo (go' values)
go' [] len = 0
go' ((l,val): other) len =
if l > len then
skipValue
else
max skipValue takeValue
where skipValue = go' other len
takeValue = val + go (len - l)
这会做一些不必要的工作,因为 takeValue
案例重新评估了完整的可销售部分,但它的速度足以通过链接网页上的所有测试案例。如果它不够快,那么你需要一个记忆器来记忆一个函数,结果在具有不同参数的调用之间共享(相同的长度,但不同的可销售部分,你知道答案无论如何都是一样的因为问题的特殊方面以及您检查不同适销件和长度的顺序)。这将是一个非标准的记忆,但修改 memo
函数来处理这种情况并不难,我不认为,只需将参数分成一个“关键”参数和一个“非键”参数,或通过在记忆时提供的任意函数从参数中导出键。