|
138楼算法的实现:欢迎提出反例或改进意见。
对递归WITH有兴趣的同学可以看看下面使用TABLE()函数的技巧。
WITH b AS (
SELECT POWER(2,ROW_NUMBER() OVER (ORDER BY time)-1) bits_id ---- 速度从快到慢,每个人编为二进制的一位
,name
,time
,ROW_NUMBER() OVER (ORDER BY time DESC) rn ---- 速度从快到慢排序的序号, 用于下面计算参考时间
,COUNT(*) OVER() cnt
,MIN(time) OVER() min_time -------- 最快的那个人过河一趟的时间
FROM bridge_crossing
)
,target AS (
---- 最快的那个人逐个陪伴其他:N-1人过河用的时间,作为参照时间。如果递归过程中发现时间已超过这个值则没必要继续
SELECT SUM(CASE WHEN MOD(rn-1,:N-1)=0 THEN time + CASE WHEN cnt-rn+1<=:N THEN 0 ELSE min_time END
END) AS estimated
FROM b
)
,c AS (---- 构造出一个集合用于笛卡尔积
SELECT 1 AS direction ------ 方向:1过河, 0返回
,LEVEL num ------ 过河人数; 从2~N逐一尝试
,1 AS sort_flag ------ 排序标记,1表示从快到慢(取最快的2~N人过河)
,1 AS bits_flag ------ 1表示未过河, 0表示已过河
FROM DUAL
WHERE LEVEL>1 CONNECT BY LEVEL<=:N
UNION ALL SELECT 1,:N,-1,1 FROM DUAL ------ 选取最慢的N个人,这也是过河必须尝试的一种情况
UNION ALL SELECT 0,1,1,-1 FROM DUAL ------ 返回,永远是取最快的已过河的人
)
,t(step,names,bits,time,cnt) AS ( ---- 递归模拟过河的每一步骤。
---- step:第几步 names:往返人员名单 bits:未过河的人的二进制位之和 time:累计用时 cnt:未过河人数
SELECT 1,CAST('' AS VARCHAR2(1000))
,POWER(2,COUNT(*))-1 ---------- 1未过河,0已过河, 一开始把所有人的二进制位都置1
,0
,COUNT(*)
FROM bridge_crossing
UNION ALL
---对COLUMN_VALUE的解释见下方。这个COLUMN_VALUE可以用标量子查询来取代,但是为了避免把一个复杂查询写很多遍,使用了TABLE()函数的技巧
---COLUMN_VALUE表示这个复杂计算的结果
---SUBSTR(COLUMN_VALUE,1,INSTR(COLUMN_VALUE,' ',1,num+1)) 取出前num个人的bits_id, 和集合b用instr条件连接,就知道哪些人在这前num个人中
SELECT t.step+1
,t.names||','||
(SELECT LISTAGG(b.name,' ') WITHIN GROUP (ORDER BY bits_id) FROM b WHERE INSTR(SUBSTR(COLUMN_VALUE,1,INSTR(COLUMN_VALUE,' ',1,c.num+1)),' '||b.bits_id||' ')>0) ---- 取出前num个人的姓名
---- 下面取出前num个人的二进制位bits_id并求和,这里也可以用 FROM DUAL CONNECT BY的办法对COLUMN_VALUE解析
,t.bits-c.bits_flag*(SELECT SUM(b.bits_id) FROM b WHERE INSTR(SUBSTR(COLUMN_VALUE,1,INSTR(COLUMN_VALUE,' ',1,c.num+1)),' '||b.bits_id||' ')>0)
---- 下面取出前num个人所用的时间并求出最大的,这是这一批人过河要用的时间
,t.time+(SELECT MAX(b.time) FROM b WHERE INSTR(SUBSTR(COLUMN_VALUE,1,INSTR(COLUMN_VALUE,' ',1,c.num+1)),' '||b.bits_id||' ')>0) ---- 过去的num个人中时间取最大
,t.cnt-c.num*c.bits_flag ------- 计算人数的变化,过河为减少,返回为增加
FROM t
,target
,c ---- 和前面生成的所有需要尝试的情况(集合c)做笛卡尔积
---下面的TABLE()函数返回一个集合,具有一个列COLUMN_VALUE, 相当于把t和c的连接结果的每一行做如下计算:
---把当前未过河的人(t.bits中二进制位为1的人)按照速度升序或降序排列,然后把这些人的bits_id用空格串成一个字符串
---如果为回程则是把当前已过河的人(t.bits中二进制位为0的人)按照速度排序,同样把bits_id串起来供后面使用
,TABLE(CAST(MULTISET(SELECT ' '||LISTAGG(b.bits_id,' ') WITHIN GROUP(ORDER BY c.sort_flag*b.time)||' '
FROM b
WHERE BITAND(t.bits,b.bits_id)=b.bits_id*c.direction ----
) AS SYS.ODCIVARCHAR2LIST
)
) ------- 这个COLUMN_VALUE
WHERE t.time<target.estimated ---- 如果递归过程中发现时间已超过这个值则没必要继续
AND t.bits<>0 ----------- 当t.bits=0则所有人已过河,为最终状态
AND MOD(t.step,2)=c.direction
AND (c.direction=1 AND (t.cnt<=:N AND c.sort_flag=1 AND c.num=t.cnt ---- 剩下不到N人了,则全部过河
OR t.cnt>:N AND (MOD(t.bits,POWER(2,:N))=0 ----- 最快的N人全部不在对岸,此时不能过最慢的N人
AND c.sort_flag=1 ----- sort_flag=1表示是2~N个最快的人
OR MOD(t.bits,POWER(2,:N))>0 ----- 如果对岸有最快的N人之中的人,则快、慢组都应该尝试
)
) ---- c.direction=1: 过河
OR c.direction=0 ---- 返回
)
)
SELECT * FROM (
SELECT names,time
FROM t
WHERE t.bits=0
ORDER BY time
)
WHERE ROWNUM=1
;
|
|