row_number 和 cte 使用实例:背包问题

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

row_number 和 cte 使用实例:背包问题_第1张图片
然后,老顾的思路非常简单啊,就是用 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

row_number 和 cte 使用实例:背包问题_第2张图片
然后,还是用 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

row_number 和 cte 使用实例:背包问题_第3张图片
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

row_number 和 cte 使用实例:背包问题_第4张图片

解决同一行数据需要引用两次的问题

在背包问题中,二维数组解法的最关键的一个步骤,就是需要比较当物品加入后的价值和原价值进行对比,不明白的小伙伴,自行搜索 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

row_number 和 cte 使用实例:背包问题_第5张图片
这个时候,我们只需要拿到最后一行数据中的 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 中按这个数量扩展即可。

小结

老顾就是闲的,嘿嘿,至于更多的背包问题结果,比如选中了哪些物品之类的,老顾这里就不再列出了,有兴趣的小伙伴可以自行解决了哦。

我们这次因为有了上一次监考安排的经验,所以这次还是很顺利的就完成了,可喜可贺可喜可贺。

你可能感兴趣的:(sql,背包,01背包,背包问题,cte,for,xml)