|
此题newkid解题思路总结公布如下,另此贴还附上此解题思路的文档下载,方便大家查阅:
本期题目是在上期基础上要求去除重复解。重复的定义在题目中已经说得很清楚了,现在的任务是如何判断两个解是否重复。如果我们能够找到一种方法,为两个重复的解赋予同样的一个特征值,那么只要算一下总共有多少个不同的特征值COUNT(DISTINCT)就可以了。通过观察我们可以得到八种变形(连同自身)的座标变换公式,如果两个答案重复,它们变换出来的8种将会是同一个集合,只是集合中的顺序可能不同。不重复的答案则变换之后也没有交集。通过给这个集合排序,取其中最大或最小值,可以判断是否重复。重复的答案将会取到相同的最大(或最小)值,这个就是我们要的特征值。
上述结论没有严格证明,数学爱好者可以做一下证明。
技术上实现八种变换的难度不大,只是比较繁琐。因为无非就是座标的换算,我们先把所有解法编号answer_id,每个解法拆开为N*N行,每行带有0,1值及其座标,然后对其座标做8种变换运算,再按运算后的顺序重新组合为一个字符串。这样原来的一个字符串就变成了八个,每个字符串都对应原来的解法编号answer_id。只要用MIN(或者MAX)... GROUP BY answer_id就可以得到这个特征值,再进行计数即可。
下面就是这个思路的5X5放两个球的示例:
- WITH d AS ( ---------- 一行里放两个球,这两个球所处的列位置的所有组合。这里尝试了和上面不一样的写法。
- SELECT s ---- 下面的c1到c5表示该列有没有球
- ,SUBSTR(s,1,1) c1,SUBSTR(s,2,1) c2,SUBSTR(s,3,1) c3,SUBSTR(s,4,1) c4,SUBSTR(s,5,1) c5
- FROM (SELECT RPAD(LPAD('1',n1,'0')||LPAD('1',n2-n1,'0'),:N,'0') s ---- LPAD, RPAD用于在空位置填入0
- FROM (SELECT ROWNUM n1 FROM DUAL CONNECT BY ROWNUM<=:N)
- ,(SELECT ROWNUM n2 FROM DUAL CONNECT BY ROWNUM<=:N)
- WHERE n1<n2
- )
- )
- ,m AS (
- SELECT answer_id
- ,SUBSTR(str,seq,1) AS val
- ,CEIL(seq/5) r ---- 行号
- ,MOD(seq-1,5)+1 c ---- 列号
- FROM (SELECT ROWNUM answer_id
- ,d1.s||d2.s||d3.s||d4.s||d5.s str
- FROM d d1,d d2,d d3,d d4,d d5 -------- 下面d1-d5表示第1-5行。这些行里面的球的位置,都来自于d
- WHERE d1.c1+d2.c1+d3.c1+d4.c1+d5.c1=2 ------- 竖线,所有行的第一列的总球数
- AND d1.c2+d2.c2+d3.c2+d4.c2+d5.c2=2
- AND d1.c3+d2.c3+d3.c3+d4.c3+d5.c3=2
- AND d1.c4+d2.c4+d3.c4+d4.c4+d5.c4=2
- AND d1.c5+d2.c5+d3.c5+d4.c5+d5.c5=2
- AND d1.c3+d2.c2+d3.c1 <=2 ------- 右上到左下的斜线, 13,22,31 ---- 坐标13表示1行3列,下同
- AND d1.c4+d2.c3+d3.c2+d4.c1 <=2 ------- 右上到左下的斜线, 14,23,32,41
- AND d1.c5+d2.c4+d3.c3+d4.c2+d5.c1<=2 ------- 右上到左下的斜线, 15,24,33,42,51
- AND d2.c5+d3.c4+d4.c3+d5.c2<=2 ------- 右上到左下的斜线, 25,34,43,52
- AND d3.c5+d4.c4+d5.c3<=2 ------- 右上到左下的斜线, 35,44,53
- AND d1.c3+d2.c4+d3.c5 <=2 ------- 左上到右下的斜线, 13,24,35
- AND d1.c2+d2.c3+d3.c4+d4.c5 <=2 ------- 左上到右下的斜线, 12,23,34,45
- AND d1.c1+d2.c2+d3.c3+d4.c4+d5.c5<=2 ------- 左上到右下的斜线, 11,22,33,44,55
- AND d2.c1+d3.c2+d4.c3+d5.c4<=2 ------- 左上到右下的斜线, 21,32,43,54
- AND d3.c1+d4.c2+d5.c3<=2 ------- 左上到右下的斜线, 31,42,53
- )
- ,(SELECT ROWNUM seq FROM DUAL CONNECT BY ROWNUM<=25) ---- 用于把1行拆为
- )
- ,m2 AS (
- SELECT answer_id,0 AS m_type,val,r,c FROM m ----- m_type=0: 未经变换
- UNION ALL
- SELECT answer_id,1 AS m_type,val,r,6-c FROM m ----- m_type=1: 沿竖线中轴对折
- UNION ALL
- SELECT answer_id,2 AS m_type,val,6-r,c FROM m ----- m_type=2: 沿横线中轴对折
- UNION ALL
- SELECT answer_id,3 AS m_type,val,6-r,6-c FROM m ----- m_type=3: 2,3 的叠加, 或顺时针180度
- UNION ALL
- SELECT answer_id,4 AS m_type,val,c,r FROM m ----- m_type=4: 以左上至右下斜线为轴对折
- UNION ALL
- SELECT answer_id,5 AS m_type,val,6-c,6-r FROM m ----- m_type=5: 以右上至左下斜线为轴对折
- UNION ALL
- SELECT answer_id,6 AS m_type,val,c,6-r FROM m ----- m_type=6: 顺时针90度
- UNION ALL
- SELECT answer_id,7 AS m_type,val,6-c,r FROM m ----- m_type=7: 逆时针90度
- )
- ,m3 AS (
- SELECT answer_id
- ,MIN(SYS_CONNECT_BY_PATH(val,' ')) matrix_str --- 用CONNECT BY进行字符串拼接,取最小值作为8种变换的共同特征值
- FROM m2
- WHERE r=5 AND c=5
- START WITH c=1 AND r=1
- CONNECT BY answer_id = PRIOR answer_id
- AND m_type = PRIOR m_type
- AND (r = PRIOR r AND c = PRIOR c+1
- OR r = PRIOR r+1 AND c=1 AND PRIOR c=5
- )
- GROUP BY answer_id
- )
- SELECT COUNT(DISTINCT matrix_str) distinct_cnt,COUNT(*) all_cnt
- FROM m3;
- 上述思路可以改良一下,当我们把一个解拆为N*N行时,那些没有球的格子可以过滤掉,这样一个解只拆成10行,在做座标变换的时候计算量会少一些。采用这方法,最后拼字符串就有所不同,因为我们只有1没有0, 下面展示了一种利用十进制的加法来拼字符串的技巧。
- WITH d AS ( ---------- 一行里放M个球,这M个球所处的列位置的所有组合
- SELECT s ---- 下面的c1到c5表示该列有没有球
- ,SUBSTR(s,1,1) c1,SUBSTR(s,2,1) c2,SUBSTR(s,3,1) c3,SUBSTR(s,4,1) c4,SUBSTR(s,5,1) c5
- FROM (SELECT REPLACE(SYS_CONNECT_BY_PATH(x,','),',') s
- FROM (SELECT 1 x FROM DUAL UNION ALL SELECT 0 x FROM DUAL)
- WHERE LEVEL=:N
- CONNECT BY LEVEL<=:N
- )
- WHERE LENGTH(s)-NVL(LENGTH(REPLACE(s,'1')),0)=:M
- )
- ,m AS (
- SELECT answer_id
- ,CEIL(seq/5) r ---- 行号
- ,MOD(seq-1,5)+1 c ---- 列号
- FROM (SELECT ROWNUM answer_id
- ,d1.s||d2.s||d3.s||d4.s||d5.s str
- FROM d d1,d d2,d d3,d d4,d d5 -------- 下面d1-d5表示第1-5行。这些行里面的球的位置,都来自于d
- WHERE d1.c1+d2.c1+d3.c1+d4.c1+d5.c1=2 ------- 竖线,所有行的第一列的总球数
- AND d1.c2+d2.c2+d3.c2+d4.c2+d5.c2=2
- AND d1.c3+d2.c3+d3.c3+d4.c3+d5.c3=2
- AND d1.c4+d2.c4+d3.c4+d4.c4+d5.c4=2
- AND d1.c5+d2.c5+d3.c5+d4.c5+d5.c5=2
- AND d1.c3+d2.c2+d3.c1 <=2 ------- 右上到左下的斜线, 13,22,31 ---- 坐标13表示1行3列,下同
- AND d1.c4+d2.c3+d3.c2+d4.c1 <=2 ------- 右上到左下的斜线, 14,23,32,41
- AND d1.c5+d2.c4+d3.c3+d4.c2+d5.c1<=2 ------- 右上到左下的斜线, 15,24,33,42,51
- AND d2.c5+d3.c4+d4.c3+d5.c2<=2 ------- 右上到左下的斜线, 25,34,43,52
- AND d3.c5+d4.c4+d5.c3<=2 ------- 右上到左下的斜线, 35,44,53
- AND d1.c3+d2.c4+d3.c5 <=2 ------- 左上到右下的斜线, 13,24,35
- AND d1.c2+d2.c3+d3.c4+d4.c5 <=2 ------- 左上到右下的斜线, 12,23,34,45
- AND d1.c1+d2.c2+d3.c3+d4.c4+d5.c5<=2 ------- 左上到右下的斜线, 11,22,33,44,55
- AND d2.c1+d3.c2+d4.c3+d5.c4<=2 ------- 左上到右下的斜线, 21,32,43,54
- AND d3.c1+d4.c2+d5.c3<=2 ------- 左上到右下的斜线, 31,42,53
- )
- ,(SELECT ROWNUM seq FROM DUAL CONNECT BY ROWNUM<=25) ---- 用于把1行拆为
- WHERE SUBSTR(str,seq,1)=1 ------- 只留下有球的格子
- )
- ,m2 AS (
- SELECT answer_id,0 AS m_type,r,c FROM m ----- m_type=0: 未经变换
- UNION ALL
- SELECT answer_id,1 AS m_type,r,6-c FROM m ----- m_type=1: 沿竖线中轴对折
- UNION ALL
- SELECT answer_id,2 AS m_type,6-r,c FROM m ----- m_type=2: 沿横线中轴对折
- UNION ALL
- SELECT answer_id,3 AS m_type,6-r,6-c FROM m ----- m_type=3: 2,3 的叠加, 或顺时针180度
- UNION ALL
- SELECT answer_id,4 AS m_type,c,r FROM m ----- m_type=4: 以左上至右下斜线为轴对折
- UNION ALL
- SELECT answer_id,5 AS m_type,6-c,6-r FROM m ----- m_type=5: 以右上至左下斜线为轴对折
- UNION ALL
- SELECT answer_id,6 AS m_type,c,6-r FROM m ----- m_type=6: 顺时针90度
- UNION ALL
- SELECT answer_id,7 AS m_type,6-c,r FROM m ----- m_type=7: 逆时针90度
- )
- ,m3 AS (
- SELECT MIN(s) AS matrix_str -- 取最小值作为8种变换的共同特征值
- FROM (SELECT answer_id,m_type
- ,LPAD(SUM(POWER(10,:N*(:N-r) + :N-c)),:N*:N,'0') s ---- 小技巧:利用10的幂对该位置1,间接达到拼字符串的目的, 假设所有位数仍在NUMBER支持之内
- FROM m2
- GROUP BY answer_id,m_type
- )
- GROUP BY answer_id
- )
- SELECT COUNT(DISTINCT matrix_str) distinct_cnt,COUNT(*) all_cnt
- FROM m3;
- 上述方法都是把一个解拆开后再作座标运算,然后又拼回来。有没有一种方法可以不拆开,一下子就完成字符串的重排列呢?这里我要隆重推荐〇〇发明的TRANSLATE办法:
- TRANSLATE(expr, from_string, to_string)
- 它的作用是把一个字符串expr里面的字符作一个映射变换,而原来的字符顺序保持不变。如果expr是from_string的重新排列,会怎么样呢?它其实相当于会把to_string重新排列!
- 观察下列例子:
- SELECT TRANSLATE('bdca','abcd','1234') from dual;
- TRAN
- ----
- 2431
- 假设abcd是对原排列顺序的一种抽象表示,bdca表示我们要的新排列顺序,它对1234的作用就是重新排成了2431。这实在是太巧妙了。以5X5为例,我们可以用abcdefghijklmnopqrstuvwxy表示原顺序,把其他7种顺序手工列举出来,作为TRANSLATE的模版:
- WITH d AS ( ---------- 一行里放M个球,这M个球所处的列位置的所有组合
- SELECT s ---- 下面的c1到c5表示该列有没有球
- ,SUBSTR(s,1,1) c1,SUBSTR(s,2,1) c2,SUBSTR(s,3,1) c3,SUBSTR(s,4,1) c4,SUBSTR(s,5,1) c5
- FROM (SELECT REPLACE(SYS_CONNECT_BY_PATH(x,','),',') s
- FROM (SELECT 1 x FROM DUAL UNION ALL SELECT 0 x FROM DUAL)
- WHERE LEVEL=:N
- CONNECT BY LEVEL<=:N
- )
- WHERE LENGTH(s)-NVL(LENGTH(REPLACE(s,'1')),0)=:M
- )
- ,m AS ( SELECT ROWNUM answer_id
- ,d1.s||d2.s||d3.s||d4.s||d5.s s
- FROM d d1,d d2,d d3,d d4,d d5 -------- 下面d1-d5表示第1-5行。这些行里面的球的位置,都来自于d
- WHERE d1.c1+d2.c1+d3.c1+d4.c1+d5.c1=2 ------- 竖线,所有行的第一列的总球数
- AND d1.c2+d2.c2+d3.c2+d4.c2+d5.c2=2
- AND d1.c3+d2.c3+d3.c3+d4.c3+d5.c3=2
- AND d1.c4+d2.c4+d3.c4+d4.c4+d5.c4=2
- AND d1.c5+d2.c5+d3.c5+d4.c5+d5.c5=2
- AND d1.c3+d2.c2+d3.c1 <=2 ------- 右上到左下的斜线, 13,22,31 ---- 坐标13表示1行3列,下同
- AND d1.c4+d2.c3+d3.c2+d4.c1 <=2 ------- 右上到左下的斜线, 14,23,32,41
- AND d1.c5+d2.c4+d3.c3+d4.c2+d5.c1<=2 ------- 右上到左下的斜线, 15,24,33,42,51
- AND d2.c5+d3.c4+d4.c3+d5.c2<=2 ------- 右上到左下的斜线, 25,34,43,52
- AND d3.c5+d4.c4+d5.c3<=2 ------- 右上到左下的斜线, 35,44,53
- AND d1.c3+d2.c4+d3.c5 <=2 ------- 左上到右下的斜线, 13,24,35
- AND d1.c2+d2.c3+d3.c4+d4.c5 <=2 ------- 左上到右下的斜线, 12,23,34,45
- AND d1.c1+d2.c2+d3.c3+d4.c4+d5.c5<=2 ------- 左上到右下的斜线, 11,22,33,44,55
- AND d2.c1+d3.c2+d4.c3+d5.c4<=2 ------- 左上到右下的斜线, 21,32,43,54
- AND d3.c1+d4.c2+d5.c3<=2 ------- 左上到右下的斜线, 31,42,53
- )
- ,h as ( ----- 这里也可以用UNION ALL写7行,最后MIN/MAX+GROUP BY
- select 'abcdefghijklmnopqrstuvwxy' org --原始
- ,'edcbajihgfonmlktsrqpyxwvu' m1
- ,'uvwxypqrstklmnofghijabcde' m2
- ,'yxwvutsrqponmlkjihgfedcba' m3 --旋转180度
- ,'upkfavqlgbwrmhcxsnidytoje' m4 --旋转90度
- ,'ejotydinsxchmrwbglqvafkpu' m5 --旋转270度
- ,'afkpubglqvchmrwdinsxejoty' m6 --按左上右下对角线对称
- ,'ytojexsnidwrmhcvqlgbupkfa' m7 --按右上左下对角线对称
- FROM DUAL
- )
- SELECT COUNT(DISTINCT
- GREATEST(s
- ,translate(m1,org,s) -------- 各种对称位置的重排模版,每种模版都会把S里面的0和1重新排列
- ,translate(m2,org,s)
- ,translate(m3,org,s)
- ,translate(m4,org,s)
- ,translate(m5,org,s)
- ,translate(m6,org,s)
- ,translate(m7,org,s)
- )
- ) AS distinct_cnt
- ,COUNT(*) AS all_cnt
- FROM m,h
- ;
- 虽然手工写七种模版有些辛苦,但这个新思路令人振奋。其实,结合前面一种方法,我们可以自动生成这8种模版,因为它就是对任意一个字符串(原始顺序)进行座标变换然后再拼接起来。因为只有一个原始串,所以计算量很小,比原来对所有结果进行座标运算要好得多。而变换完的模版就可以被后来利用了。这种自动生成模版的方法,还可以应用于不同的M值。
- 下面就把第一期的递归WITH写法,连同这种自动生成TRANSLATE模版的思路结合起来:
- var m number;
- exec :m:=2
- var n number;
- exec :n:=6
- WITH r AS ( ---------- 一行里放M个球,这M个球所处的列位置的所有组合
- SELECT s as one_row
- FROM (SELECT REPLACE(SYS_CONNECT_BY_PATH(x,','),',') s
- FROM (SELECT 1 x FROM DUAL UNION ALL SELECT 0 x FROM DUAL)
- WHERE LEVEL=:N
- CONNECT BY LEVEL<=:N
- )
- WHERE LENGTH(s)-LENGTH(REPLACE(s,'1'))=:M
- )
- ,balls (row_cnt -- 当前行数
- ,all_rows -- 截至当前行,所有行的摆法拼接的结果
- ,col_cnt -- 把每一列看作十进制的一位,此数字表示在列方向的各位和
- ,diag_cnt1 -- 此数字表示在右上至左下的斜线上各位的和
- ,diag_cnt2 -- 此数字表示在左上至右下的斜线上各位的和
- )
- AS (
- SELECT 1,one_row,TO_NUMBER(one_row),TO_NUMBER(one_row),TO_NUMBER(one_row) ---初始化第一行数据
- FROM r
- UNION ALL
- SELECT balls.row_cnt+1 ---- 行数递增
- ,balls.all_rows||r.one_row ---当前行拼入总结果
- ,balls.col_cnt+r.one_row --- 在列方向各位进行累加
- ,balls.diag_cnt1*10+r.one_row ---- 在斜线方向各位进行累加
- ,balls.diag_cnt2+r.one_row*POWER(10,balls.row_cnt)
- FROM balls,r
- WHERE balls.row_cnt<:N ---- 递归出口
- AND INSTR(balls.col_cnt+r.one_row,:M+1)=0 ---- 把带M+1的结果拦截掉
- AND INSTR(balls.diag_cnt1*10+r.one_row,:M+1)=0
- AND INSTR(balls.diag_cnt2+r.one_row*POWER(10,balls.row_cnt),:M+1)=0
- )
- ,m AS (
- SELECT CEIL(LEVEL/:N) r
- ,MOD(LEVEL-1,:N)+1 c
- ,LEVEL seq
- FROM DUAL
- CONNECT BY LEVEL<=:N*:N
- )
- ,m2 AS (
- SELECT seq,0 AS tpl_id,r,c FROM m ----- tpl_id=0: 原样
- UNION ALL
- SELECT seq,1 AS tpl_id,r,:N+1-c FROM m ----- tpl_id=1: 沿竖线中轴对折
- UNION ALL
- SELECT seq,2 AS tpl_id,:N+1-r,c FROM m ----- tpl_id=2: 沿横线中轴对折
- UNION ALL
- SELECT seq,3 AS tpl_id,:N+1-r,:N+1-c FROM m ----- tpl_id=3: 1,2 的叠加, 或顺时针180度
- UNION ALL
- SELECT seq,4 AS tpl_id,c,r FROM m ----- tpl_id=4: 以左上至右下斜线为轴对折
- UNION ALL
- SELECT seq,5 AS tpl_id,:N+1-c,:N+1-r FROM m ----- tpl_id=5: 以右上至左下斜线为轴对折
- UNION ALL
- SELECT seq,6 AS tpl_id,c,:N+1-r FROM m ----- tpl_id=6: 顺时针90度
- UNION ALL
- SELECT seq,7 AS tpl_id,:N+1-c,r FROM m ----- tpl_id=7: 逆时针90度
- )
- ,templates AS (
- SELECT tpl_id
- ,tpl_str
- ,MAX(CASE WHEN tpl_id=0 THEN tpl_str END) OVER() AS tpl_org
- FROM (SELECT m2.tpl_id
- ,REPLACE(SYS_CONNECT_BY_PATH(all_chars.ch,'/'),'/') tpl_str ----- 拼接模版
- FROM m2
- JOIN (SELECT ROWNUM seq
- ,ch
- FROM (SELECT CHR(LEVEL) ch ----- 用ASCII码表作为模板. 0,1要排除掉,斜杠/作为分隔符也要排除
- FROM DUAL
- WHERE CHR(LEVEL) NOT IN ('0','1','/')
- CONNECT BY LEVEL<=255
- )
- ) all_chars
- ON m2.seq = all_chars.seq
- WHERE LEVEL = :N*:N
- START WITH r=1
- CONNECT BY tpl_id = PRIOR tpl_id ---- 同一种模版
- AND (PRIOR c <:N AND r=PRIOR r AND c = PRIOR c+1 ---- 同一行的下一列
- OR PRIOR c =:N AND r=PRIOR r+1 AND c = 1 ---- 下一行的第一列
- )
- )
- )
- SELECT COUNT(DISTINCT matrix_str) AS distinct_cnt, MAX(all_cnt) all_cnt
- FROM (SELECT MIN(TRANSLATE(tpl_str,tpl_org,all_rows)) AS matrix_str
- ,MAX(all_cnt) all_cnt
- FROM (SELECT ROWNUM AS answer_id
- ,all_rows
- ,COUNT(*) OVER() AS all_cnt
- FROM balls
- WHERE row_cnt=:N
- ) CROSS JOIN templates
- GROUP BY answer_id
- );
- 如果纯粹是要求不重复的解,那么有些明显是对称的解就可以事先排除掉。比如在递归的第一层,那些左右对称的很容易识别出来,一下子就砍掉一半。到最后一层,上下对称的也可以识别出来,也可以去掉一半。这样剩下的去重复工作量就小得多了。这个优化办法也是〇〇贡献出来的,优化之后执行时间差不多只有原来的一半。这个优化方法保证了所有不重复解都被覆盖到,但它的中间结果并没有求出所有的解,而仅仅是一个子集。要还原出所有的解也很简单,在8次变换之后就会覆盖所有的解,在此基础上去除重复就是了:
- WITH r AS ( ---------- 一行里放M个球,这M个球所处的列位置的所有组合
- SELECT s as one_row
- FROM (SELECT REPLACE(SYS_CONNECT_BY_PATH(x,','),',') s
- FROM (SELECT 1 x FROM DUAL UNION ALL SELECT 0 x FROM DUAL)
- WHERE LEVEL=:N
- CONNECT BY LEVEL<=:N
- )
- WHERE LENGTH(s)-LENGTH(REPLACE(s,'1'))=:M
- )
- ,balls (row_cnt -- 当前行数
- ,all_rows -- 截至当前行,所有行的摆法拼接的结果
- ,col_cnt -- 把每一列看作十进制的一位,此数字表示在列方向的各位和
- ,diag_cnt1 -- 此数字表示在右上至左下的斜线上各位的和
- ,diag_cnt2 -- 此数字表示在左上至右下的斜线上各位的和
- )
- AS (
- SELECT 1,one_row,TO_NUMBER(one_row),TO_NUMBER(one_row),TO_NUMBER(one_row) ---初始化第一行数据
- FROM r where one_row>=reverse(one_row) --左右镜像取大
- UNION ALL
- SELECT balls.row_cnt+1 ---- 行数递增
- ,balls.all_rows||r.one_row ---当前行拼入总结果
- ,balls.col_cnt+r.one_row --- 在列方向各位进行累加
- ,balls.diag_cnt1*10+r.one_row ---- 在斜线方向各位进行累加
- ,balls.diag_cnt2+r.one_row*POWER(10,balls.row_cnt)
- FROM balls,r
- WHERE balls.all_rows >= case when balls.row_cnt=:n-1 then r.one_row else '0' end --上下镜像取大
- and balls.row_cnt<:N ---- 递归出口
- AND INSTR(balls.col_cnt+r.one_row,:M+1)=0 ---- 把带M+1的结果拦截掉
- AND INSTR(balls.diag_cnt1*10+r.one_row,:M+1)=0
- AND INSTR(balls.diag_cnt2+r.one_row*POWER(10,balls.row_cnt),:M+1)=0
- )
- ,m AS (
- SELECT CEIL(LEVEL/:N) r
- ,MOD(LEVEL-1,:N)+1 c
- ,LEVEL seq
- FROM DUAL
- CONNECT BY LEVEL<=:N*:N
- )
- ,m2 AS (
- SELECT seq,0 AS tpl_id,r,c FROM m ----- tpl_id=0: 原样
- UNION ALL
- SELECT seq,1 AS tpl_id,r,:N+1-c FROM m ----- tpl_id=1: 沿竖线中轴对折
- UNION ALL
- SELECT seq,2 AS tpl_id,:N+1-r,c FROM m ----- tpl_id=2: 沿横线中轴对折
- UNION ALL
- SELECT seq,3 AS tpl_id,:N+1-r,:N+1-c FROM m ----- tpl_id=3: 1,2 的叠加, 或顺时针180度
- UNION ALL
- SELECT seq,4 AS tpl_id,c,r FROM m ----- tpl_id=4: 以左上至右下斜线为轴对折
- UNION ALL
- SELECT seq,5 AS tpl_id,:N+1-c,:N+1-r FROM m ----- tpl_id=5: 以右上至左下斜线为轴对折
- UNION ALL
- SELECT seq,6 AS tpl_id,c,:N+1-r FROM m ----- tpl_id=6: 顺时针90度
- UNION ALL
- SELECT seq,7 AS tpl_id,:N+1-c,r FROM m ----- tpl_id=7: 逆时针90度
- )
- ,templates AS (
- SELECT tpl_id
- ,tpl_str
- ,MAX(CASE WHEN tpl_id=0 THEN tpl_str END) OVER() AS tpl_org
- FROM (SELECT m2.tpl_id
- ,REPLACE(SYS_CONNECT_BY_PATH(all_chars.ch,'/'),'/') tpl_str ----- 拼接模版
- FROM m2
- JOIN (SELECT ROWNUM seq
- ,ch
- FROM (SELECT CHR(LEVEL) ch ----- 用ASCII码表作为模板. 0,1要排除掉,斜杠/作为分隔符也要排除
- FROM DUAL
- WHERE CHR(LEVEL) NOT IN ('0','1','/')
- CONNECT BY LEVEL<=255
- )
- ) all_chars
- ON m2.seq = all_chars.seq
- WHERE LEVEL = :N*:N
- START WITH r=1
- CONNECT BY tpl_id = PRIOR tpl_id ---- 同一种模版
- AND (PRIOR c <:N AND r=PRIOR r AND c = PRIOR c+1 ---- 同一行的下一列
- OR PRIOR c =:N AND r=PRIOR r+1 AND c = 1 ---- 下一行的第一列
- )
- )
- )
- SELECT COUNT(DISTINCT transformed),MAX(all_cnt)
- FROM (SELECT answer_id,MIN(transformed) transformed,MAX(all_cnt) all_cnt
- FROM (SELECT answer_id,transformed,COUNT(DISTINCT transformed) OVER() all_cnt
- FROM (SELECT answer_id,TRANSLATE(tpl_str,tpl_org,all_rows) transformed
- FROM (SELECT ROWNUM AS answer_id
- ,all_rows
- FROM balls
- WHERE row_cnt=:N
- ) CROSS JOIN templates
- )
- )
- GROUP BY answer_id
- );
- 在第一期讲评时我们提到了一种手工模拟递归WITH的方法,下面就展示这种写法,在做变换的时候对模版做了一个行转列,这样原来的8行模版就变为一行,再用LEAST或GREATEST来代替MIN/MAX, 这种做法的好处是不会产生很大的中间结果。缺点就是当采用对称优化求不重复的解,需要在此基础上再对8行的模版做一个笛卡尔积再次进行转换,还原所有的解,但转换的成本很低,值得采用。
- EXEC :M:=2;
- EXEC :N:=7;
- WITH r AS ( ---------- 一行里放M个球,这M个球所处的列位置的所有组合
- SELECT s as one_row
- FROM (SELECT REPLACE(SYS_CONNECT_BY_PATH(x,','),',') s
- FROM (SELECT 1 x FROM DUAL UNION ALL SELECT 0 x FROM DUAL)
- WHERE LEVEL=:N
- CONNECT BY LEVEL<=:N
- )
- WHERE LENGTH(s)-LENGTH(REPLACE(s,'1'))=:M
- )
- ,b1 AS (
- SELECT 1 row_num,one_row all_rows,TO_NUMBER(one_row) col_cnt,TO_NUMBER(one_row) diag_cnt1,TO_NUMBER(one_row) diag_cnt2---初始化第一行数据
- FROM r where one_row>=reverse(one_row) --左右镜像取大
- )
- ,b2 AS (
- SELECT b.row_num+1 AS row_num
- ,b.all_rows||r.one_row AS all_rows
- ,b.col_cnt+r.one_row AS col_cnt
- ,b.diag_cnt1*10+r.one_row AS diag_cnt1
- ,b.diag_cnt2+r.one_row*POWER(10,b.row_num) AS diag_cnt2
- FROM b1 b,r
- WHERE INSTR(b.col_cnt +r.one_row,:M+1)=0 ---- 把带M+1的结果拦截掉
- AND INSTR(b.diag_cnt1*10+r.one_row,:M+1)=0
- AND INSTR(b.diag_cnt2+r.one_row*POWER(10,b.row_num),:M+1)=0
- )
- ,b3 AS (
- SELECT b.row_num+1 AS row_num
- ,b.all_rows||r.one_row AS all_rows
- ,b.col_cnt+r.one_row AS col_cnt
- ,b.diag_cnt1*10+r.one_row AS diag_cnt1
- ,b.diag_cnt2+r.one_row*POWER(10,b.row_num) AS diag_cnt2
- FROM b2 b,r
- WHERE INSTR(b.col_cnt +r.one_row,:M+1)=0 ---- 把带M+1的结果拦截掉
- AND INSTR(b.diag_cnt1*10+r.one_row,:M+1)=0
- AND INSTR(b.diag_cnt2+r.one_row*POWER(10,b.row_num),:M+1)=0
- )
- ,b4 AS (
- SELECT b.row_num+1 AS row_num
- ,b.all_rows||r.one_row AS all_rows
- ,b.col_cnt+r.one_row AS col_cnt
- ,b.diag_cnt1*10+r.one_row AS diag_cnt1
- ,b.diag_cnt2+r.one_row*POWER(10,b.row_num) AS diag_cnt2
- FROM b3 b,r
- WHERE INSTR(b.col_cnt +r.one_row,:M+1)=0 ---- 把带M+1的结果拦截掉
- AND INSTR(b.diag_cnt1*10+r.one_row,:M+1)=0
- AND INSTR(b.diag_cnt2+r.one_row*POWER(10,b.row_num),:M+1)=0
- )
- ,b5 AS (
- SELECT b.row_num+1 AS row_num
- ,b.all_rows||r.one_row AS all_rows
- ,b.col_cnt+r.one_row AS col_cnt
- ,b.diag_cnt1*10+r.one_row AS diag_cnt1
- ,b.diag_cnt2+r.one_row*POWER(10,b.row_num) AS diag_cnt2
- FROM b4 b,r
- WHERE INSTR(b.col_cnt +r.one_row,:M+1)=0 ---- 把带M+1的结果拦截掉
- AND INSTR(b.diag_cnt1*10+r.one_row,:M+1)=0
- AND INSTR(b.diag_cnt2+r.one_row*POWER(10,b.row_num),:M+1)=0
- )
- ,b6 AS (
- SELECT b.row_num+1 AS row_num
- ,b.all_rows||r.one_row AS all_rows
- ,b.col_cnt+r.one_row AS col_cnt
- ,b.diag_cnt1*10+r.one_row AS diag_cnt1
- ,b.diag_cnt2+r.one_row*POWER(10,b.row_num) AS diag_cnt2
- FROM b5 b,r
- WHERE :n>=6 and b.all_rows >= case when :n=6 then r.one_row else '0' end --如果N=6,上下对称取大
- and INSTR(b.col_cnt +r.one_row,:M+1)=0 ---- 把带M+1的结果拦截掉
- AND INSTR(b.diag_cnt1*10+r.one_row,:M+1)=0
- AND INSTR(b.diag_cnt2+r.one_row*POWER(10,b.row_num),:M+1)=0
- union all
- select * from b5 where :n<6 -- 若N=5,则b6就是b5的结果集
- )
- ,b7 AS (
- SELECT b.row_num+1 AS row_num
- ,b.all_rows||r.one_row AS all_rows
- ,b.col_cnt+r.one_row AS col_cnt
- ,b.diag_cnt1*10+r.one_row AS diag_cnt1
- ,b.diag_cnt2+r.one_row*POWER(10,b.row_num) AS diag_cnt2
- FROM b6 b,r
- WHERE :n>=7 and b.all_rows >= r.one_row --如果N=7,上下对称取大
- AND INSTR(b.col_cnt +r.one_row,:M+1)=0 ---- 把带M+1的结果拦截掉
- AND INSTR(b.diag_cnt1*10+r.one_row,:M+1)=0
- AND INSTR(b.diag_cnt2+r.one_row*POWER(10,b.row_num),:M+1)=0
- union all
- select * from b6 where :n<7 -- 若N=6,则b7就是b6的结果集
- )
- ,m AS (
- SELECT CEIL(LEVEL/:N) r
- ,MOD(LEVEL-1,:N)+1 c
- ,LEVEL seq
- FROM DUAL
- CONNECT BY LEVEL<=:N*:N
- )
- ,m2 AS (
- SELECT seq,0 AS tpl_id,r,c FROM m ----- tpl_id=0: 原样
- UNION ALL
- SELECT seq,1 AS tpl_id,r,:N+1-c FROM m ----- tpl_id=1: 沿竖线中轴对折
- UNION ALL
- SELECT seq,2 AS tpl_id,:N+1-r,c FROM m ----- tpl_id=2: 沿横线中轴对折
- UNION ALL
- SELECT seq,3 AS tpl_id,:N+1-r,:N+1-c FROM m ----- tpl_id=3: 1,2 的叠加, 或顺时针180度
- UNION ALL
- SELECT seq,4 AS tpl_id,c,r FROM m ----- tpl_id=4: 以左上至右下斜线为轴对折
- UNION ALL
- SELECT seq,5 AS tpl_id,:N+1-c,:N+1-r FROM m ----- tpl_id=5: 以右上至左下斜线为轴对折
- UNION ALL
- SELECT seq,6 AS tpl_id,c,:N+1-r FROM m ----- tpl_id=6: 顺时针90度
- UNION ALL
- SELECT seq,7 AS tpl_id,:N+1-c,r FROM m ----- tpl_id=7: 逆时针90度
- )
- ,templates AS (
- SELECT tpl_id
- ,tpl_str
- ,MAX(CASE WHEN tpl_id=0 THEN tpl_str END) OVER() AS tpl_org
- FROM (SELECT m2.tpl_id
- ,REPLACE(SYS_CONNECT_BY_PATH(all_chars.ch,'/'),'/') tpl_str ----- 拼接模版
- FROM m2
- JOIN (SELECT ROWNUM seq
- ,ch
- FROM (SELECT CHR(LEVEL) ch ----- 用ASCII码表作为模板. 0,1要排除掉,斜杠/作为分隔符也要排除
- FROM DUAL
- WHERE CHR(LEVEL) NOT IN ('0','1','/')
- CONNECT BY LEVEL<=255
- )
- ) all_chars
- ON m2.seq = all_chars.seq
- WHERE LEVEL = :N*:N
- START WITH r=1
- CONNECT BY tpl_id = PRIOR tpl_id ---- 同一种模版
- AND (PRIOR c <:N AND r=PRIOR r AND c = PRIOR c+1 ---- 同一行的下一列
- OR PRIOR c =:N AND r=PRIOR r+1 AND c = 1 ---- 下一行的第一列
- )
- )
- )
- ,temp2 AS (
- SELECT MAX(DECODE(tpl_id,0,tpl_str)) tpl0
- ,MAX(DECODE(tpl_id,1,tpl_str)) tpl1
- ,MAX(DECODE(tpl_id,2,tpl_str)) tpl2
- ,MAX(DECODE(tpl_id,3,tpl_str)) tpl3
- ,MAX(DECODE(tpl_id,4,tpl_str)) tpl4
- ,MAX(DECODE(tpl_id,5,tpl_str)) tpl5
- ,MAX(DECODE(tpl_id,6,tpl_str)) tpl6
- ,MAX(DECODE(tpl_id,7,tpl_str)) tpl7
- FROM templates
- )
- ,dist as(
- SELECT DISTINCT
- LEAST(all_rows
- ,TRANSLATE(tpl1,tpl0,all_rows)
- ,TRANSLATE(tpl2,tpl0,all_rows)
- ,TRANSLATE(tpl3,tpl0,all_rows)
- ,TRANSLATE(tpl4,tpl0,all_rows)
- ,TRANSLATE(tpl5,tpl0,all_rows)
- ,TRANSLATE(tpl6,tpl0,all_rows)
- ,TRANSLATE(tpl7,tpl0,all_rows)
- )
- AS all_rows
- FROM b7 CROSS JOIN temp2)
- SELECT :N,:M,MAX(distinct_cnt),COUNT(*)
- FROM (
- SELECT DISTINCT
- TRANSLATE(tpl_str,tpl_org,all_rows) all_rows
- ,distinct_cnt
- FROM (SELECT all_rows,count(*) OVER() AS distinct_cnt
- FROM dist
- ) CROSS JOIN templates -------- 把不重复解再次和模板进行笛卡尔连接,还原出其他解
- )
- ;
- 这个写法在 N=7, M=3的时候还能跑出答案,其他写法都崩溃了:
- :N :M MAX(DISTINCT_CNT) COUNT(*)
- ---------- ---------- ----------------- ----------
- 7 3 164991 1319016
- 已用时间: 00: 09: 54.24
复制代码 |
|