row_number 和 cte 使用实例:背包问题
- 背包问题
- 01背包
- 解决同一行数据需要引用两次的问题
- 对 for xml 的结果进行引用时的处理
- 完全背包
- 多重背包
- 小结
背包问题
最近老顾从新把算法捡了起来,碰到了各种各样以前没见过的,工作中没遇到的问题,很多问题老顾都是一头雾水,完全没有思路,比如。。。背包问题?
背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?它是在1978年由Merkle和Hellman提出的。
好家伙。。。。这问题够古老的,老顾78年才生人啊。
在看到各种题解之前,说实话,老顾完全不会解这个,太苦恼了,只能学别人的题解,而且因为学历问题,公式也看不懂,仅仅做到了会用这个解法罢了。
关于背包问题的描述,大家可以自行到百度百科了解,总之呢,算法很简单,实现很容易,开发语言表示这都不是事。
01背包
在使用了各种编程语言用各种算法实现了背包的解法后,老顾突然想到,数据库是否可以实现?不用流程控制?嘿嘿,Sqlserver 表示不服!
在各种背包解法里,老顾选择了二维数组方式,因为物品,重量,价值,这些加起来就是一个二维关系表了,所以,用这个方式不需要大动干戈了。
假设有5个物品 A、B、C、D、E,分别重10、12、11、16、10,分别价值12、16、61、24、15,问,在总计40承重的空间内,最大价值是多少?
这是一个最简单的 01 背包问题,每个物品只能用一次。很好,我们先用 cte 把原始数据弄出来。
;with t as (
select 'A' name,10 w,12 p
union all select 'B',12,16
union all select 'C',11,16
union all select 'D',16,24
union all select 'E',10,15
)
select * from t
然后,老顾的思路非常简单啊,就是用 cte 递归,来实现背包计算,不过,看过老顾上一篇文章《row_number 和 cte 使用实例:考场监考安排》都知道,cte 的限制很多很多,不能多次递归,不能用外关联查询,不能列转行行转列等等等等。所以,老顾的变通方法,就是用 for xml 进行递归之间的数据传递。
那么,这次实现 01 背包问题也同样是如此。为了避免物品名有重复的问题,我们还是用 row_number 来排下序。
with ...
,t1 as (
select *,row_number() over(order by name) rid from t
)
select * from t1
然后,还是用 master…spt_values 做数据填充,先实现第一个物品的背包数据。
with ...
select * from t1
cross apply (
select number,(case when number<w then 0 else p end) cp
from master..spt_values
where type='p'
and number<=40
) b
where rid=1
number 就表示称重的数量,我们可以看到当 number 与 w 相同的时候,背包内的价值数量就发生变动了。那么,如果我们用4个left join ,其实就可以完成背包的后续计算了,不过需要记得当新物品加入时,需要计算多物品价值,和原价值中,取最大的罢了。
不过,因为是要解决 01 背包问题,咱们最后的目的是具有泛用性,所以不确定到底有多少物品,所以用 join 方式解决就变得不可行了。还是要回到 cte 递归。而为了传递数据,我们需要将价值数据,作为参数传递给下一次使用,所以,还是得用 for xml。
with ...
select *
from t1
cross apply (
select stuff((
select ',' + convert(varchar,(case when number < w then 0 else p end))
from master..spt_values
where type ='p' and number<=@place
order by number
for xml path('')
),1,1,'') cols
) b
where rid=1
解决同一行数据需要引用两次的问题
在背包问题中,二维数组解法的最关键的一个步骤,就是需要比较当物品加入后的价值和原价值进行对比,不明白的小伙伴,自行搜索 01 背包问题,很多大佬都有填表格的解法说明。
而 cte 递归中,无法多次引用递归内容,所以,我们这里用 for xml 解决了,对 for xml 的数据进行拆分,从新变成多行数据。而这个数据引用,是可以无限次使用的哦。
比如,现在我们的 cols 列的数据是:
0,0,0,0,0,0,0,0,0,0,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12
表示40个格子的价值,这是一个字符串数据了,需要拆分成40行的数值类型数据。这里有几个办法:
1、高版本的SqlServer自带 string_split,需要 SqlServer 2016 以上,在切割后用 row_number 追加一下行号。
2、自行制作一个字符串切割函数,对版本没有要求,最好能自带行号,如果没有,也用 row_number 追加行号
3、使用正则,需要 SqlServer 2005 以上,开 clr
这里一定要追加行号哦,这个行号就是对应的格子承重哦,需要注意,用 row_number 追加行号的,需要减去1,否则承重计算会有误差的哦。
那么最后的递归部分完成后,查询指令就如下了。
declare @place int
set @place = 40
;with t as (
select 'A' name,10 w,12 p
union all select 'B',12,16
union all select 'C',11,16
union all select 'D',16,24
union all select 'E',10,15
),tf as (
select a.*
from t a
cross apply (
select number
from master..spt_values
where number<=(@place/w) and type='p'
) b
),t1 as (
select *,row_number() over(order by name) rid from t
),t2 as (
select *
from t1
cross apply (
select stuff((
select ',' + convert(varchar,(case when number < w then 0 else p end))
from master..spt_values
where type ='p' and number<=@place
order by number
for xml path('')
),1,1,'') cols
) b
where rid=1
union all
select name,w,p,rid,ncols
from (
select a.*,cols
from t1 a,t2 b
where b.rid+1=a.rid
) a
cross apply (
select stuff((
select ',' + convert(varchar,(case
when m1.sn - 1 < a.w -- 这里注意要有一个减 1 哦
then convert(int,m1.match) -- 承重小于当前物品重量,继承上一次的价值数
else -- 否则,该格子的价值,应该为上次价值数和加入该物品价值后的较大的那一个
(case
when m2.rp + a.p > convert(int,m1.match)
then m2.rp + a.p
else convert(int,m1.match)
end)
end))
from master..RegexMatches(cols,'\d+') m1 -- 老顾这里是用正则切割的字符串,有需要正则的,可以查阅老顾以前的 sql 文章,或者在评论区扣老顾
outer apply ( -- 再次切割一次,按照背包计算的方式,使承重减去当前物品重量,与原有承重价值对齐,方便计算当前物品加入后的价值和原价值
select isnull(convert(int,match),0) rp
from master..regexmatches(cols,'\d+')
where m1.sn - a.w = sn
) m2
order by m1.sn
for xml path('')
),1,1,'') ncols
) b
)
select * from t2
order by rid
这个时候,我们只需要拿到最后一行数据中的 cols 中的最后一个数据,那就是最大价值了。
对 for xml 的结果进行引用时的处理
这里再补充一下,老顾用正则切出来的数据格式
select * from master..regexmatches('0,0,0,0,0,0,0,0,0,0,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12','\d+')
sn match index
----------- ---------- -----------
1 0 0
2 0 2
3 0 4
4 0 6
5 0 8
6 0 10
7 0 12
8 0 14
9 0 16
10 0 18
11 12 20
12 12 23
13 12 26
14 12 29
15 12 32
16 12 35
17 12 38
18 12 41
19 12 44
20 12 47
21 12 50
22 12 53
23 12 56
24 12 59
25 12 62
26 12 65
27 12 68
28 12 71
29 12 74
30 12 77
31 12 80
32 12 83
33 12 86
34 12 89
35 12 92
36 12 95
37 12 98
38 12 101
39 12 104
40 12 107
41 12 110
(41 行受影响)
这次背包问题中,只用到了前两列,即sn和match列,用 string_split 的小伙伴,只需要生成两列的数据,分别表示 number 和 cp 列即可,老顾在递归里没有改列名,直接使用了,小伙伴可以自行修改,即可得到结果了。
完全背包
完全背包问题和 01 背包问题的区别在于,01 背包中,每个物品只能用 1 次,而完全背包则不限制数量,那么完全背包的问题可以转化成 01 背包,直接在 cte 里插入一个数据扩展部分即可。
declare @place int
set @place = 40
;with t as (
select 'A' name,10 w,12 p
union all select 'B',12,16
union all select 'C',11,16
union all select 'D',16,24
union all select 'E',10,15
),tf as ( -- 插入一个临时表,用来计算如果承重完全放同一种物品时,每个物品最多能放多少个
select a.*
from t a
cross apply (
select number
from master..spt_values
where number<=(@place/w) and type='p'
) b
),t1 as ( -- 排序的数据来源,从原始表变成扩展表
select *,row_number() over(order by name) rid from tf
),t2 as (
select *
from t1
cross apply (
select stuff((
select ',' + convert(varchar,(case when number < w then 0 else p end))
from master..spt_values
where type ='p' and number<=@place
order by number
for xml path('')
),1,1,'') cols
) b
where rid=1
union all
select name,w,p,rid,ncols
from (
select a.*,cols
from t1 a,t2 b
where b.rid+1=a.rid
) a
cross apply (
select stuff((
select ',' + convert(varchar,(case
when m1.sn - 1 < a.w
then convert(int,m1.match)
else
(case
when m2.rp + a.p > convert(int,m1.match)
then m2.rp + a.p
else convert(int,m1.match)
end)
end))
from master..RegexMatches(cols,'\d+') m1
outer apply (
select isnull(convert(int,match),0) rp
from master..regexmatches(cols,'\d+')
where m1.sn - a.w = sn
) m2
order by m1.sn
for xml path('')
),1,1,'') ncols
) b
)
select * from t2
order by rid
多重背包
。。。。。。我连完全背包都弄出来了,你问我多重背包?
多重背包的问题和完全背包又有一点点区别,就是每个物品有最大数量。。。。
得,这个问题老顾就不解答了,直接修改 cte 表 t 的部分,将数量给出,然后 tf 中按这个数量扩展即可。
小结
老顾就是闲的,嘿嘿,至于更多的背包问题结果,比如选中了哪些物品之类的,老顾这里就不再列出了,有兴趣的小伙伴可以自行解决了哦。
我们这次因为有了上一次监考安排的经验,所以这次还是很顺利的就完成了,可喜可贺可喜可贺。