ITPUB论坛-中国最专业的IT技术社区

 
 注册
热搜:
查看: 46418|回复: 32

[精华] ITPUB第3届“盛拓传媒杯”SQL数据库编程大赛第2题及附加题参考解题思路

[复制链接]
论坛徽章:
480
榜眼
日期:2015-09-09 10:34:21秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12状元
日期:2015-11-23 10:04:09举人
日期:2015-11-23 10:04:09
跳转到指定楼层
1#
发表于 2015-12-19 05:38 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
附加题M,N,K的情况复杂得多,如果沿用原来的思路,中间的游戏树十分庞大,就连4,4,3的结果也要跑40分钟才能出来。

我做了两项优化,先简单说明一下优化思路:
1. 原来3X3的游戏树中保留了前后棋局的对应关系,由于一种棋局可以由多种变化而来,每个棋局就会有多行记录。而实际上第二题的答案仅和当前棋局相关,和上手棋局并无关系,所以新的MNK代码中改变了分析函数的PARTITION BY表达式,只为每种棋局保留一行数据。到了第二步的推理,再根据当前棋局把所有的上手棋局生成出来。
2. 即使应用了上述优化办法,游戏树还是巨大无比。此时游戏树中有许多棋局是对称的,但它们的结果都一样,所以在1的基础上,把每个棋局的所有对称都算出来,然后用LEAST留下一种,作为分析函数的PARTITION BY表达式。

上述优化的代价也是很大的,代码复杂了很多,目前也不能确定有没有bug,因为没有足够的测试用例,先帖在这里,以后有时间再完善。有兴趣的人可以继续玩。


VAR M NUMBER;
VAR N NUMBER;
VAR K NUMBER;

EXEC :M:=4;
EXEC :N:=4;
EXEC :K:=3;

VAR V_BOARD VARCHAR2(20);
exec :v_board:='----------------';

exec :v_board:='-----X----------';

WITH
cells AS (
  SELECT g.*
        ,CASE WHEN (:M=:N AND c>=r OR :M<>:N) AND c<=CEIL(:M/2) AND r<=CEIL(:N/2) THEN 1 ELSE 0 END AS start_point ---- 如果是空棋盘,只需选择start_point=1作为起点,其他都是对称的
   FROM (
         SELECT LEVEL pos,POWER(2,LEVEL-1) b,CEIL(LEVEL/:N) r,MOD(LEVEL-1,:N)+1 c FROM DUAL CONNECT BY LEVEL<=:M*:N
        ) g
)
,w (win_bits,cnt,r,c,direction) as (
--- win_bits表示三个连续棋子的二进制之和; cnt表示累计几个棋子,满三个即停;r,c为行列坐标,direction表示哪个方向
SELECT b,1,r,c,direction FROM cells,(SELECT LEVEL direction FROM DUAL CONNECT BY LEVEL<=4) ----从棋盘m任选一点开始,和四种方向做笛卡尔积
UNION ALL
SELECT w.win_bits+cells.b,w.cnt+1,cells.r,cells.c ,w.direction
FROM w,cells
WHERE w.cnt<:K ----满K个即停止
      ----- 下面从四个方向来加入棋子。并不是每个起点都能得到连续三个棋子,下面的坐标关系自然会过滤掉不合格的
      AND (direction=1 AND cells.c=w.c AND cells.r=w.r+1 -----如果方向1, 则加入同列、下一行的位子
           OR direction=2 AND cells.r=w.r AND cells.c=w.c+1 -----如果方向2, 则加入同行、下一列的位子
           OR direction=3 AND cells.r=w.r+1 AND cells.c=w.c+1 -----如果方向3, 则加入下一行、下一列的位子(右下方)
           OR direction=4 AND cells.r=w.r+1 AND cells.c=w.c-1 -----如果方向4, 则加入下一行、上一列的位子(左下方)
           )
)
,win as (select win_bits from w where cnt=:K)
,m2 AS ( --------- 对所有坐标进行八种对称变换
SELECT pos,0 AS tpl_id,r,c FROM cells       ----- tpl_id=0: 原样
UNION ALL
SELECT pos,1 AS tpl_id,r,:N+1-c FROM cells       ----- tpl_id=1: 沿竖线中轴翻转
UNION ALL
SELECT pos,2 AS tpl_id,:M+1-r,c FROM cells       ----- tpl_id=2: 沿横线中轴翻转
UNION ALL
SELECT pos,3 AS tpl_id,:M+1-r,:N+1-c FROM cells       ----- tpl_id=3: 1,2 的叠加, 或顺时针180度
UNION ALL
SELECT pos,4 AS tpl_id,c,r FROM cells       ----- tpl_id=4: 以左上至右下对角线为轴翻转
UNION ALL
SELECT pos,5 AS tpl_id,:N+1-c,:M+1-r FROM cells       ----- tpl_id=5: 以右上至左下对角线为轴翻转
UNION ALL
SELECT pos,6 AS tpl_id,c,:M+1-r FROM cells       ----- tpl_id=6: 顺时针90度
UNION ALL
SELECT pos,7 AS tpl_id,:N+1-c,r FROM cells       ----- tpl_id=7: 逆时针90度
)
,tmpl AS ( ------ 生成去除对称的TRANSLATE模版
SELECT tp0,tp1,tp2,tp3
      ,DECODE(:M,:N,tp4,tp0) tp4 ------ 如果棋盘不是正方形,那些行列对调的模版就不能用
      ,DECODE(:M,:N,tp5,tp0) tp5
      ,DECODE(:M,:N,tp6,tp0) tp6
      ,DECODE(:M,:N,tp7,tp0) tp7
  FROM (
        SELECT MAX(DECODE(tpl_id,0,tpl_str)) tp0
              ,MAX(DECODE(tpl_id,1,tpl_str)) tp1
              ,MAX(DECODE(tpl_id,2,tpl_str)) tp2
              ,MAX(DECODE(tpl_id,3,tpl_str)) tp3
              ,MAX(DECODE(tpl_id,4,tpl_str)) tp4
              ,MAX(DECODE(tpl_id,5,tpl_str)) tp5
              ,MAX(DECODE(tpl_id,6,tpl_str)) tp6
              ,MAX(DECODE(tpl_id,7,tpl_str)) tp7
          FROM (SELECT m2.tpl_id
                      ,REPLACE(SYS_CONNECT_BY_PATH(all_chars.ch,'/'),'/') tpl_str ----- 拼接模版
                  FROM m2
                       JOIN (        SELECT LEVEL pos
                                           ,CHR(64+LEVEL) ch ----- 用ASCII码表作为模板,从A开始
                                       FROM DUAL
                                      CONNECT BY LEVEL<=160
                            ) all_chars
                       ON m2.pos = all_chars.pos
                 WHERE LEVEL = :M*: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  ---- 下一行的第一列
                                )
               )
        )
)
,tp AS (
SELECT tmpl.*        
      ,REPLACE(:V_BOARD,'-','_') m0
      ,TRANSLATE(tp0,tp1,REPLACE(:V_BOARD,'-','_')) m1 ---- 生成各种对称的LIKE表达式, 用于判断所生成的上手棋局是否由根变换而来
      ,TRANSLATE(tp0,tp2,REPLACE(:V_BOARD,'-','_')) m2
      ,TRANSLATE(tp0,tp3,REPLACE(:V_BOARD,'-','_')) m3
      ,TRANSLATE(tp0,tp4,REPLACE(:V_BOARD,'-','_')) m4
      ,TRANSLATE(tp0,tp5,REPLACE(:V_BOARD,'-','_')) m5
      ,TRANSLATE(tp0,tp6,REPLACE(:V_BOARD,'-','_')) m6
      ,TRANSLATE(tp0,tp7,REPLACE(:V_BOARD,'-','_')) m7
  FROM tmpl
)       
-----------把当前局面当作根节点,生成当前局面往下的游戏树
,t(board,x,o,step,winner,rn) AS (
SELECT CAST(board AS VARCHAR2(80)) board
      ,x,o
      ,step
      ,CASE WHEN MOD(step,2)=1 THEN (SELECT 'X' FROM win WHERE BITAND(x,win.win_bits)=win.win_bits AND ROWNUM=1)
            ELSE (SELECT 'O' FROM win WHERE BITAND(o,win.win_bits)=win.win_bits AND ROWNUM=1)
       END  AS  winner
      ,1
  FROM (SELECT board
              ,NVL(SUM(CASE WHEN SUBSTR(board,pos,1)='X' THEN cells.b END),0) x
              ,NVL(SUM(CASE WHEN SUBSTR(board,pos,1)='O' THEN cells.b END),0) o
              ,:M*:N-REGEXP_COUNT(:v_board,'-') step
         FROM cells
             ,(SELECT LEAST(:V_BOARD
                      ,TRANSLATE(tp0,tp1,:V_BOARD)
                      ,TRANSLATE(tp0,tp2,:V_BOARD)
                      ,TRANSLATE(tp0,tp3,:V_BOARD)
                      ,TRANSLATE(tp0,tp4,:V_BOARD)
                      ,TRANSLATE(tp0,tp5,:V_BOARD)
                      ,TRANSLATE(tp0,tp6,:V_BOARD)
                      ,TRANSLATE(tp0,tp7,:V_BOARD)
                      ) AS board
                 FROM tp
              )
        GROUP BY board
       )
UNION ALL
SELECT b.column_value
      ,CASE WHEN MOD(t.step,2)=0 THEN t.x+cells.b ELSE t.x END
      ,CASE WHEN MOD(t.step,2)=1 THEN t.o+cells.b ELSE t.o END
      ,t.step+1
      ,CASE WHEN MOD(t.step,2)=0 THEN (SELECT 'X' FROM win WHERE BITAND(t.x+cells.b,win.win_bits)=win.win_bits AND ROWNUM=1)
            ELSE (SELECT 'O' FROM win WHERE BITAND(t.o+cells.b,win.win_bits)=win.win_bits AND ROWNUM=1)
       END  AS  winner
      ,ROW_NUMBER() OVER(PARTITION BY LEAST(b.column_value,TRANSLATE(tp0,tp1,b.column_value),TRANSLATE(tp0,tp2,b.column_value),TRANSLATE(tp0,tp3,b.column_value) ------- 每种对称棋局只留下一行
                                            ,TRANSLATE(tp0,tp4,b.column_value),TRANSLATE(tp0,tp5,b.column_value),TRANSLATE(tp0,tp6,b.column_value),TRANSLATE(tp0,tp7,b.column_value))
                         ORDER BY b.column_value
                        )
  FROM t,
       cells,
       tp,
       TABLE(CAST(MULTISET(
                    SELECT SUBSTR(t.board,1,cells.pos-1)||CASE WHEN MOD(t.step,2)=0 THEN 'X' ELSE 'O' END||SUBSTR(t.board,cells.pos+1) str
                    FROM DUAL
                          ) AS SYS.ODCIVARCHAR2LIST)) b      
WHERE BITAND(t.x+t.o,cells.b)=0
       AND t.winner IS NULL
       AND (t.step=0 AND cells.start_point=1  ----针对空棋盘的一个优化
            OR t.step>0
            )
       AND rn=1
)
,s (board,step,winner,rn,iterator,steps_to_win,player) as (
---从所有获胜终局反推, 把winner一步一步带到上层,最终判断是否能带到根
select DISTINCT LEAST(board,TRANSLATE(tp0,tp1,board),TRANSLATE(tp0,tp2,board),TRANSLATE(tp0,tp3,board),TRANSLATE(tp0,tp4,board),TRANSLATE(tp0,tp5,board),TRANSLATE(tp0,tp6,board),TRANSLATE(tp0,tp7,board))
      ,step,winner,1
      ,max(step) over() AS iterator ---- 递归起点,从最高步数往前倒推
      ,0,DECODE(MOD(step,2),1,'X','O') player
  from t,tp
where winner is not null AND rn=1
union all
select CASE WHEN s.step<>s.iterator THEN s.board ELSE b.COLUMN_VALUE END
      ,CASE WHEN s.step<>s.iterator THEN s.step ELSE s.step-1 END
      ,case when s.step<>s.iterator THEN s.winner
            ELSE NVL(MAX(CASE when s.player=s.winner THEN s.winner END)
                         OVER(PARTITION BY b.COLUMN_VALUE), ---- 如果这一层的下子的一方(player)有胜局则优先取
                     ------ 否则,计算有几种去除对称后的后续棋局,如果和对手获胜局相等则说明对手胜
                     CASE WHEN count(distinct s.board) over(partition by b.COLUMN_VALUE)= --REGEXP_COUNT(b.COLUMN_VALUE,'-') --- 等于所有可能的对手走法
                           (SELECT count(distinct least(REGEXP_REPLACE(b.COLUMN_VALUE,'-',s.player,1,LEVEL)
                                                       ,TRANSLATE(tp0,tp1,REGEXP_REPLACE(b.COLUMN_VALUE,'-',s.player,1,LEVEL))
                                                       ,TRANSLATE(tp0,tp2,REGEXP_REPLACE(b.COLUMN_VALUE,'-',s.player,1,LEVEL))
                                                       ,TRANSLATE(tp0,tp3,REGEXP_REPLACE(b.COLUMN_VALUE,'-',s.player,1,LEVEL))
                                                       ,TRANSLATE(tp0,tp4,REGEXP_REPLACE(b.COLUMN_VALUE,'-',s.player,1,LEVEL))
                                                       ,TRANSLATE(tp0,tp5,REGEXP_REPLACE(b.COLUMN_VALUE,'-',s.player,1,LEVEL))
                                                       ,TRANSLATE(tp0,tp6,REGEXP_REPLACE(b.COLUMN_VALUE,'-',s.player,1,LEVEL))
                                                       ,TRANSLATE(tp0,tp7,REGEXP_REPLACE(b.COLUMN_VALUE,'-',s.player,1,LEVEL))
                                                       )
                                       )
                              FROM tp
                             CONNECT BY LEVEL<=:M*:N-s.step+1
                            )
                          then s.winner
                     END
                     ---- 如果以上都不满足则WINNER为空,说明双方都没有必胜策略
                     )
       END
       ----- 本轮递归结束后,所有同一个prior_board之下的winner都一致,由上述公式推出
       ----- ROW_NUMBER用于取出同一个prior_board之下的其中一行进入下轮递归,如果走子方有必胜则取步数最少的,否则取最多的
      ,ROW_NUMBER() OVER(PARTITION BY case when s.step<>s.iterator THEN s.board ELSE b.COLUMN_VALUE END
                         ORDER BY CASE when s.player=s.winner THEN 1 ELSE 2 END ---- 如果这一层的下子的一方(player)有胜局则优先取
                                 ,s.steps_to_win*CASE when s.player=s.winner THEN 1 ELSE -1 END ---- 如果是对手,要取最坏情况(最多步数)
                        ) AS rn
      ,s.iterator-1
      ,case when s.step<>s.iterator OR s.player<>s.winner  
            THEN s.steps_to_win
            else s.steps_to_win+1
       end
      ,CASE WHEN s.step<>s.iterator THEN s.player ELSE DECODE(s.player,'X','O','X') END
  from s left join  -------------- 为了使得下次递归能看到所有获胜局,必须用外连接带入下层
         TABLE(CAST(MULTISET( ------ 由当前棋局生成上手棋局,方法是将所有已经下过的子用 '-' 代替
           SELECT LEAST(REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL)
                       ,TRANSLATE(tp0,tp1,REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL))
                       ,TRANSLATE(tp0,tp2,REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL))
                       ,TRANSLATE(tp0,tp3,REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL))
                       ,TRANSLATE(tp0,tp4,REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL))
                       ,TRANSLATE(tp0,tp5,REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL))
                       ,TRANSLATE(tp0,tp6,REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL))
                       ,TRANSLATE(tp0,tp7,REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL))
                       )
                FROM tp
           WHERE REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL) LIKE m0 ---- 生成出来的上手棋局必须是从根衍变出来的,否则没有必要计算
                 OR REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL) LIKE m1
                 OR REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL) LIKE m2
                 OR REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL) LIKE m3
                 OR REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL) LIKE m4
                 OR REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL) LIKE m5
                 OR REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL) LIKE m6
                 OR REGEXP_REPLACE(S.BOARD,PLAYER,'-',1,LEVEL) LIKE m7
            CONNECT BY LEVEL<=CEIL(s.STEP/2)
         ) AS SYS.ODCIVARCHAR2LIST)) b
       on s.step=s.iterator -- AND b.COLUMN_VALUE LIKE (REPLACE(:V_BOARD,'-','_'))
        ---- 正在考察的当前步, 只有这一步才需要连接board获取上一手棋局(PRIOR_BOARD)
WHERE s.winner is not null and s.rn=1 and s.iterator>:M*:N-REGEXP_COUNT(:v_board,'-') and s.step<=s.iterator
)
SELECT winner||steps_to_win  FROM s,tp
where s.iterator=:M*:N-REGEXP_COUNT(:v_board,'-')
      and rn=1 AND winner IS NOT NULL;
论坛徽章:
480
榜眼
日期:2015-09-09 10:34:21秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12状元
日期:2015-11-23 10:04:09举人
日期:2015-11-23 10:04:09
2#
 楼主| 发表于 2015-12-19 05:37 | 只看该作者
本帖最后由 newkid 于 2015-12-21 22:45 编辑

活动见:/thread-1943911-1-1.html
第一题见:/thread-2048956-1-1.html

第二题解题思路:

从一个给定局面,如果局面上已经能够看到三子连线,那么很显然胜负已分,直接给出答案,所需步数=0。

如果没有三子连线,但是所有空位都已有棋子,则返回和棋。

否则,先看看轮到谁走下一步:如果总棋子数为奇数则轮到O, 否则轮到X。我们把轮到的人称为当前棋手。当前棋手有权选择任意一个空位下子,有N个空位就会衍生出N种直接的后续棋局。

第二题的答案可以通过观察这些空位得来。

想像一下,有个军师把N种后续棋局的答案都搞到手了,于是N个空位都被标上了获胜者以及步数。那么为了回答第二题,只需按如下顺序查看这N种后续棋局:
A.如果存在当前棋手必胜的棋局,那么最终胜者为当前棋手,步数则是各种获胜走法中最少的步数加一。因为当前棋手有主动权选择对他最有利的走法。
B.如果不存在当前棋手必胜的棋局,但存在和棋,那么最终答案为和棋。
C.如果N种结局都是对手取胜,那么答案是对手胜,步数则取最大的获胜步数。因为当前棋手会选择支撑最久的走法,而步数只算胜者的,不用加一。

现在,问题就被转化为如何获得N种后续棋局的答案。很显然,获得一种后续棋局的结果,和我们的第二题是同一个问题,只不过输入参数多了一个棋子。“军师”就是我们自己,这就是一个自己调用自己的递归函数,递归的终点就是出现直接获胜的走法或者和棋。

用PLSQL函数实现如下,如果是12C的版本,把函数放在WITH子句中就已经完成任务了:
CREATE OR REPLACE FUNCTION F_TTT(p_board IN VARCHAR2) RETURN VARCHAR2
AS
   TYPE t_str IS TABLE OF VARCHAR2(20);
   v_win t_str := t_str( --- 枚举8种获胜棋局,用于LIKE匹配,下划线表示匹配任一字符
         'XXX______'
        ,'___XXX___'
        ,'______XXX'
        ,'X__X__X__'
        ,'_X__X__X_'
        ,'__X__X__X'
        ,'X___X___X'
        ,'__X_X_X__'
   );
   v_current_player VARCHAR2(1);  --- 当前棋手, X或O
   v_cnt NUMBER;       ---- 空位数
   v_ret VARCHAR2(20);  ---- 后续棋局的结果
   v_min_step NUMBER := 999; ----- 后续棋局结果中最小的获胜步数
   v_pos NUMBER;  ---- 每个空位所在的位置
   v_last_player VARCHAR2(1); ----- 上一手棋手,即给定棋局的最后落子方
   v_max_step NUMBER :=-1;  ----- 后续棋局结果中最大的获胜步数
   v_draw BOOLEAN := FALSE;  ----后续棋局中是否存在和局
BEGIN
   
   v_cnt := REGEXP_COUNT(p_board,'-');  ---- 还有几个空位
   v_last_player := CASE WHEN MOD(LENGTH(p_board)-v_cnt,2)=0 THEN 'O' ELSE 'X' END;  ---- 根据总棋子数的奇偶性,判断谁是上一手的棋手
   FOR i IN 1..v_win.COUNT LOOP  ----- 循环所有获胜布局并一一尝试匹配
       IF p_board LIKE REPLACE(v_win(i),'X',v_last_player) THEN ----- 如果匹配成功,则上手棋手获胜,步数为零
          RETURN v_last_player||'0';
       END IF;
   END LOOP;
   IF v_cnt=0 THEN  -------- 如果胜负未分,且没有空位,返回和棋
      RETURN 'D';
   END IF;
   
   v_current_player := CASE v_last_player WHEN 'X' THEN 'O' ELSE 'X' END;  ---- 求出当前棋手
   FOR i IN 1..v_cnt LOOP  -----查看所有空位,一一尝试结果
       IF LENGTH(p_board)=v_cnt AND i IN (1,2,5) OR LENGTH(p_board)<>v_cnt THEN  --如果LENGTH(p_board)=空位总数则为全空棋盘
          ---- 对空棋盘的优化,全空棋盘只需考虑1,2,5的后续下法,其他对称下法的结果都在其中
          v_pos := INSTR(p_board,'-',1,i);  
          v_ret := F_TTT(SUBSTR(p_board,1,v_pos-1)|| v_current_player ||SUBSTR(p_board,v_pos+1));  ---- 尝试在第i个空位下子,递归调用自身,获得该后续棋局的结果
          CASE
          WHEN SUBSTR(v_ret,1,1)=v_current_player THEN ----- 如果结果中有当前棋手能取胜的,记下最小步数
              v_min_step := LEAST(v_min_step,TO_NUMBER(SUBSTR(v_ret,2)));
          WHEN SUBSTR(v_ret,1,1)='D' THEN ---- 如果没有取胜则看和棋是否存在
              v_draw := TRUE;
          ELSE ---- 如果是对手胜,则记下最大步数
              v_max_step := GREATEST(v_max_step,TO_NUMBER(SUBSTR(v_ret,2)));
          END CASE;
       END IF;
   END LOOP;
        
   ----- 全部空位尝试完毕,开始判断
   CASE
   WHEN v_min_step<999 THEN  ----- 如果结果中有当前棋手能取胜的,取步数最小的那种,加一步返回
        RETURN v_current_player||(v_min_step+1);
   WHEN v_draw THEN ---如果不能取胜但存在和棋,则最终为和棋
        RETURN 'D';
   ELSE RETURN v_last_player||v_max_step;  ----否则对手胜,步数取最大的
   END CASE;

END F_TTT;




但是,在SQL中是无法直接使用这个思路的,即使是递归WITH 子查询,它也不允许你“预支”(即在表达式中引用)未来的结果,而只能从已知的结果一步步推断出未知的。所以我们只能把任务划分成两部分:
1. 从给定棋局开始,根据所有空位生成所有直接后续的棋局,然后又从后续棋局再生成更深一层(更多一子)的后续棋局,……直至一方获胜或者和棋。
这其实就是第一题的做法,这些棋局被称为游戏树。

2. 在步骤1的结果中,只有树叶可以看到谁胜谁负,那些中间棋局的胜负结果是未知的。我们可以根据这些叶节点向上反推,得到前一步的结果。
叶子节点的深度也不一致,有的下一两个子就可以获胜,有的要下到最后才分胜负。我们从最深的一层开始,每次往上推理一层,一直把答案带到初始棋局的那一层。每一层的推理都要依赖于其后续棋局的结论。

推理的方法如前所述:
找出当前棋手;
考察当前棋局的所有直接后续棋局:
如果有当前棋手获胜的则把当前棋局的胜者标注为当前棋手,取步数最小的加1;
否则,如果对手获胜的后续棋局的总数等于所有后续棋局数,即全部都是对手胜,则把当前棋局的胜者标为对手,取步数最大的;
否则,当前棋局的胜者为空,即和棋。

继续往上一层推理, 直至到了顶层(给定棋局)。


下面就来看看第一步的实现SQL,和第一题基本相同但又有所区别。第一题是讲究下子顺序的,而第二题只关心胜负答案。为了给第二步推理提供数据,它在游戏树中必须保留上手棋局(PRIOR_BOARD)和当前棋局(BOARD)的对应关系。同一个上手棋局又可能是由几种不同的更上一手棋局衍变来的,这我们就不用管了,所以在游戏树中我们只需要为每一对(prior_board,board)保留一行数据。这个可以用分析函数ROW_NUMBER()来过滤,在生成每层棋局的时候就按棋局进行分区,递归时在WHERE加入rn=1的条件,就过滤掉了上层的重复棋局。生成游戏树的递归WITH如下:

t(board  ----- 到目前为止所有X,O的分布
  ,x ------- X下过了哪些位置,用二进制之和表示
  ,o ------- O下过了哪些位置,用二进制之和表示
  ,step ----- 当前走了几步(双方一共下了几子)
  ,winner ----- 如果胜负已分,在这里标出
  ,prior_board ----- 指明当前BOARD是从哪一个变化而来
  ,rn    ------- 利用分析函数ROW_NUMBER()为每一对(prior_board,board)仅仅保留一行数据
  )
AS (
------ 从给定棋局开始生成游戏树
SELECT CAST(:v_board AS VARCHAR2(9)) board
      ,x
      ,o
      ,step
      ,CASE WHEN MOD(step,2)=1 THEN (SELECT 'X' FROM win WHERE BITAND(x,win.win_bits)=win.win_bits AND ROWNUM=1) ----看看此棋局是否已分胜负
            ELSE (SELECT 'O' FROM win WHERE BITAND(o,win.win_bits)=win.win_bits AND ROWNUM=1)
       END  AS  winner
      ,CAST('' AS VARCHAR2(9)) prior_board
      ,1
  FROM (------ 从给定棋局计算X,O的二进制的值
        SELECT NVL(SUM(CASE WHEN SUBSTR(:v_board,m,1)='X' THEN m.b END),0) x
              ,NVL(SUM(CASE WHEN SUBSTR(:v_board,m,1)='O' THEN m.b END),0) o
              ,9-REGEXP_COUNT(:v_board,'-') step
         FROM m
       )
UNION ALL
SELECT SUBSTR(t.board,1,m.m-1)  ------ 把选中的下一个棋子按照其位置拼到棋盘中去。其位置即m.m表示的1-9的整数
       ||CASE WHEN MOD(t.step,2)=0 THEN 'X' ELSE 'O' END ----- 根据当前步数算出下一个子应该是X或者O
       ||SUBSTR(t.board,m.m+1)  AS board
      ,CASE WHEN MOD(t.step,2)=0 THEN t.x+m.b ELSE t.x END AS X---- 如果下一子是X, 把其二进制叠加到t.x,否则保持原样
      ,CASE WHEN MOD(t.step,2)=1 THEN t.o+m.b ELSE t.o END AS O---- 对O的处理,方法同上
      ,t.step+1
      ,CASE WHEN MOD(t.step,2)=0
            THEN (SELECT 'X' FROM win WHERE BITAND(t.x+m.b,win.win_bits)=win.win_bits AND ROWNUM=1) ---- 如果轮到X走子,用BITAND判断走后是否出现胜局
            ELSE (SELECT 'O' FROM win WHERE BITAND(t.o+m.b,win.win_bits)=win.win_bits AND ROWNUM=1) ---- 判断O是否获胜,方法同X
       END   AS  winner      
      ,t.board AS prior_board
       ------ 下面的PARTITION BY按照当前棋局以及拼出的下手棋局来分区,保留任意一种都可以,所以ORDER BY 1
      ,ROW_NUMBER() OVER(PARTITION BY t.board,SUBSTR(t.board,1,m.m-1)||CASE WHEN MOD(t.step,2)=0 THEN 'X' ELSE 'O' END||SUBSTR(t.board,m.m+1) ORDER BY 1)
  FROM t JOIN m ON BITAND(t.x+t.o,m.b)=0
WHERE t.winner IS NULL
       AND (t.step=0 AND m.m IN (1,2,5)  ----针对空棋盘的一个优化:起点只需考虑1,2,5,其他都是对称的
            OR t.step>0
            )
       AND rn=1   -----每一对(board,prior_board)只需保留一种,其他都是重复
)
, boards AS (
----- 保留所有的上手棋局与当前棋局的对应关系
SELECT board,prior_board,winner,step
FROM t
WHERE rn=1
)

第二部分的推理从最深的叶子节点开始,每次减一层。在下列SQL中用迭代变量iterator表示当前正在推理第几层。起点的胜负是直接取自游戏树的树叶,不用计算,递归生成的是更上层的WINNER标记。

递归WITH的起点:
,s (board,step,winner,rn,iterator,steps_to_win,player) as (
---从所有获胜终局反推, 把winner一步一步带到上层,最终判断是否能带到根
select board
      ,step
      ,winner
      ,1    as rn
      ,max(step) over() AS iterator ---- 推理起点,从最高步数往前倒推,每层递归减 1
      ,0    as steps_to_win
      ,DECODE(MOD(step,2),1,'X','O') player
  from boards
where winner is not null ------ 选取胜负已分的叶子节点作为递归起点
group by board,step,winner  ----- 因为boards里面保存着前后棋局的对应关系,所以board不是唯一的,必须去除重复

为了简化推理逻辑,只有胜负已分的局面(WINNER IS NOT NULL)才需要在递归过程中保留,而和棋可以从后续棋局的数量推断出来: 如果"后续棋局总数"大于"能分胜负的后续棋局数",则说明存在和棋。后续棋局总数即空位的数量,可以用REGEXP_COUNT计算有多少个'-'。

递归WITH还有一个限制,就是它的递归表名(下例中的S集合)只能在FROM中引用一次,此时看到的S数据是上次递归的结果。而我们需要考察的不仅仅有前一个iterator的推理结果,还得考察当前层数的叶子(游戏树中WINNER IS NOT NULL的那些记录),这两部分数据加起来才能决定下次递归的结果。为了达到这个目的,我们的S集合从递归起点开始就要带着所有的树叶,并且每次递归都要这些叶子,而s中的列,仅当 步数step=s.iterator的才需要重新算,如果不是当前考察的层数(s.step<>s.iterator),则保留原值。将所有数据带入下次递归的方法是用外连接。

递归部分:
union all
select CASE WHEN s.step<>s.iterator THEN s.board ELSE b.prior_board END  as board ---- 递归的下一层,是从boards表得来的上一手棋局
      ,CASE WHEN s.step<>s.iterator THEN s.step ELSE s.step-1 END  as step
      ,case when s.step<>s.iterator THEN s.winner
            ELSE NVL(MAX(CASE when s.player=s.winner THEN s.winner END)
                         OVER(PARTITION BY b.prior_board), ---- 如果这一层的下子的一方(player)有胜局则优先取
                     ------ 当上述MAX取到的值为NULL, 意味着只有平局或者对手胜。
                     ------ 下面的count(distinct b.board)表示对手取胜的数量,REGEXP_COUNT(b.prior_board,'-')则表示有多少空位数,即多少种后续棋局
                     ------ 如果两个数量相等,说明不管怎么走都是对手赢,则把s.winner,即对手带到上层
                     CASE WHEN count(distinct b.board) over(partition by b.prior_board)=REGEXP_COUNT(b.prior_board,'-') --- 等于所有可能的对手走法
                          then s.winner
                     END
                     ---- 如果以上都不满足则WINNER为空,说明双方都没有必胜策略,为和棋
                     )
       END as winner
       ----- 本轮递归结束后,所有同一个prior_board之下的winner都一致,由上述公式推出
       ----- ROW_NUMBER用于取出同一个prior_board之下的其中一行进入下轮递归,如果走子方必胜,则取步数最少的,否则取最多的
      ,ROW_NUMBER() OVER(PARTITION BY case when s.step<>s.iterator THEN s.board ELSE b.prior_board END ---- 分析函数的分区是按照本次递归后的棋局,每个局面保留一行
                         ORDER BY CASE when s.player=s.winner THEN 1 ELSE 2 END ---- 如果这一层的下子的一方(player)有胜局,则优先取
                                 ,s.steps_to_win*CASE when s.player=s.winner THEN 1 ELSE -1 END ---- 如果是对手,要取最坏情况(最多步数),所以乘以-1之后再排序
                        ) AS rn
      ,s.iterator-1
      ,case when s.step<>s.iterator OR s.player<>s.winner  
            THEN s.steps_to_win ---- 如果对手赢,步数不用递增
            else s.steps_to_win+1 ---- 如果当前棋手赢,步数要加1,因为要再走一步才到达那个局面
       end
      ,CASE WHEN s.step<>s.iterator THEN s.player ELSE DECODE(s.player,'X','O','X') END
  from s left join boards b -------------- 为了使得下次递归能看到所有获胜局,必须用外连接带入下层
       on s.step=s.iterator ---- 正在考察的当前步, 只有这一步才需要连接board获取上一手棋局(PRIOR_BOARD)
          and s.board=b.board  ---- 连接boards获得上一步棋局
WHERE s.winner is not null and s.rn=1 and s.iterator>9-REGEXP_COUNT(:v_board,'-') and s.step<=s.iterator
)

把上述几步拼起来,完整的SQL如下所示:

WITH
m AS (SELECT LEVEL m,POWER(2,LEVEL-1) b,CEIL(LEVEL/3) r,MOD(LEVEL-1,3)+1 c FROM DUAL CONNECT BY LEVEL<=9)
,w (b,win_bits,cnt,r,c,t) as (
SELECT b,b,1,r,c,t FROM m,(SELECT LEVEL t FROM DUAL CONNECT BY LEVEL<=4)
UNION ALL
SELECT m.b,w.win_bits+m.b,w.cnt+1,m.r,m.c ,w.t
FROM w,m
WHERE w.cnt<3
      AND (t=1 AND m.c=w.c AND m.r=w.r+1
           OR t=2 AND m.r=w.r AND m.c=w.c+1
           OR t=3 AND m.r=w.r+1 AND m.c=w.c+1
           OR t=4 AND m.r=w.r+1 AND m.c=w.c-1
           )
)
,win as (select win_bits from w where cnt=3)
-----------把当前局面当作根节点,生成当前局面往下的游戏树
,t(board  ----- 到目前为止所有X,O的分布
  ,x ------- X下过了哪些位置,用二进制之和表示
  ,o ------- O下过了哪些位置,用二进制之和表示
  ,step ----- 当前走了几步(双方一共下了几子)
  ,winner ----- 如果胜负已分,在这里标出
  ,prior_board ----- 指明当前BOARD是从哪一个变化而来
  ,rn    ------- 利用分析函数ROW_NUMBER()为每一对(prior_board,board)仅仅保留一行数据
  )
AS (
------ 从给定棋局开始生成游戏树
SELECT CAST(:v_board AS VARCHAR2(9)) board
      ,x
      ,o
      ,step
      ,CASE WHEN MOD(step,2)=1 THEN (SELECT 'X' FROM win WHERE BITAND(x,win.win_bits)=win.win_bits AND ROWNUM=1) ----看看此棋局是否已分胜负
            ELSE (SELECT 'O' FROM win WHERE BITAND(o,win.win_bits)=win.win_bits AND ROWNUM=1)
       END  AS  winner
      ,CAST('' AS VARCHAR2(9)) prior_board
      ,1
  FROM (------ 从给定棋局计算X,O的二进制的值
        SELECT NVL(SUM(CASE WHEN SUBSTR(:v_board,m,1)='X' THEN m.b END),0) x
              ,NVL(SUM(CASE WHEN SUBSTR(:v_board,m,1)='O' THEN m.b END),0) o
              ,9-REGEXP_COUNT(:v_board,'-') step
         FROM m
       )
UNION ALL
SELECT SUBSTR(t.board,1,m.m-1)  ------ 把选中的下一个棋子按照其位置拼到棋盘中去。其位置即m.m表示的1-9的整数
       ||CASE WHEN MOD(t.step,2)=0 THEN 'X' ELSE 'O' END ----- 根据当前步数算出下一个子应该是X或者O
       ||SUBSTR(t.board,m.m+1)  AS board
      ,CASE WHEN MOD(t.step,2)=0 THEN t.x+m.b ELSE t.x END AS X---- 如果下一子是X, 把其二进制叠加到t.x,否则保持原样
      ,CASE WHEN MOD(t.step,2)=1 THEN t.o+m.b ELSE t.o END AS O---- 对O的处理,方法同上
      ,t.step+1
      ,CASE WHEN MOD(t.step,2)=0
            THEN (SELECT 'X' FROM win WHERE BITAND(t.x+m.b,win.win_bits)=win.win_bits AND ROWNUM=1) ---- 如果轮到X走子,用BITAND判断走后是否出现胜局
            ELSE (SELECT 'O' FROM win WHERE BITAND(t.o+m.b,win.win_bits)=win.win_bits AND ROWNUM=1) ---- 判断O是否获胜,方法同X
       END   AS  winner      
      ,t.board AS prior_board
       ------ 下面的PARTITION BY按照当前棋局以及拼出的下手棋局来分区,保留任意一种都可以,所以ORDER BY 1
      ,ROW_NUMBER() OVER(PARTITION BY t.board,SUBSTR(t.board,1,m.m-1)||CASE WHEN MOD(t.step,2)=0 THEN 'X' ELSE 'O' END||SUBSTR(t.board,m.m+1) ORDER BY 1)
  FROM t JOIN m ON BITAND(t.x+t.o,m.b)=0
WHERE t.winner IS NULL
       AND (t.step=0 AND m.m IN (1,2,5)  ----针对空棋盘的一个优化:起点只需考虑1,2,5,其他都是对称的
            OR t.step>0
            )
       AND rn=1   -----每一对(board,prior_board)只需保留一种,其他都是重复
)
, boards AS (
----- 保留所有的上手棋局与当前棋局的对应关系
SELECT board,prior_board,winner,step
FROM t
WHERE rn=1
)
,s (board,step,winner,rn,iterator,steps_to_win,player) as (
---从所有获胜终局反推, 把winner一步一步带到上层,最终判断是否能带到根
select board
      ,step
      ,winner
      ,1    as rn
      ,max(step) over() AS iterator ---- 推理起点,从最高步数往前倒推,每层递归减 1
      ,0    as steps_to_win
      ,DECODE(MOD(step,2),1,'X','O') player
  from boards
where winner is not null ------ 选取胜负已分的叶子节点作为递归起点
group by board,step,winner  ----- 因为boards里面保存着前后棋局的对应关系,所以board不是唯一的,必须去除重复
union all
select CASE WHEN s.step<>s.iterator THEN s.board ELSE b.prior_board END  as board ---- 递归的下一层,是从boards表得来的上一手棋局
      ,CASE WHEN s.step<>s.iterator THEN s.step ELSE s.step-1 END  as step
      ,case when s.step<>s.iterator THEN s.winner
            ELSE NVL(MAX(CASE when s.player=s.winner THEN s.winner END)
                         OVER(PARTITION BY b.prior_board), ---- 如果这一层的下子的一方(player)有胜局则优先取
                     ------ 当上述MAX取到的值为NULL, 意味着只有平局或者对手胜。
                     ------ 下面的count(distinct b.board)表示对手取胜的数量,REGEXP_COUNT(b.prior_board,'-')则表示有多少空位数,即多少种后续棋局
                     ------ 如果两个数量相等,说明不管怎么走都是对手赢,则把s.winner,即对手带到上层
                     CASE WHEN count(distinct b.board) over(partition by b.prior_board)=REGEXP_COUNT(b.prior_board,'-') --- 等于所有可能的对手走法
                          then s.winner
                     END
                     ---- 如果以上都不满足则WINNER为空,说明双方都没有必胜策略,为和棋
                     )
       END as winner
       ----- 本轮递归结束后,所有同一个prior_board之下的winner都一致,由上述公式推出
       ----- ROW_NUMBER用于取出同一个prior_board之下的其中一行进入下轮递归,如果走子方必胜,则取步数最少的,否则取最多的
      ,ROW_NUMBER() OVER(PARTITION BY case when s.step<>s.iterator THEN s.board ELSE b.prior_board END ---- 分析函数的分区是按照本次递归后的棋局,每个局面保留一行
                         ORDER BY CASE when s.player=s.winner THEN 1 ELSE 2 END ---- 如果这一层的下子的一方(player)有胜局,则优先取
                                 ,s.steps_to_win*CASE when s.player=s.winner THEN 1 ELSE -1 END ---- 如果是对手,要取最坏情况(最多步数),所以乘以-1之后再排序
                        ) AS rn
      ,s.iterator-1
      ,case when s.step<>s.iterator OR s.player<>s.winner  
            THEN s.steps_to_win ---- 如果对手赢,步数不用递增
            else s.steps_to_win+1 ---- 如果当前棋手赢,步数要加1,因为要再走一步才到达那个局面
       end
      ,CASE WHEN s.step<>s.iterator THEN s.player ELSE DECODE(s.player,'X','O','X') END
  from s left join boards b -------------- 为了使得下次递归能看到所有获胜局,必须用外连接带入下层
       on s.step=s.iterator ---- 正在考察的当前步, 只有这一步才需要连接board获取上一手棋局(PRIOR_BOARD)
          and s.board=b.board  ---- 连接boards获得上一步棋局
WHERE s.winner is not null and s.rn=1 and s.iterator>9-REGEXP_COUNT(:v_board,'-') and s.step<=s.iterator
)
SELECT NVL(MAX(winner||steps_to_win),'D') FROM s where board=:v_board and rn=1 AND winner IS NOT NULL;

使用道具 举报

回复
招聘 : 系统分析师
论坛徽章:
477
本田
日期:2014-01-05 16:51:44技术图书徽章
日期:2014-04-21 10:26:402014年世界杯参赛球队: 伊朗
日期:2014-05-23 10:41:312014年世界杯参赛球队: 比利时
日期:2014-06-17 12:09:43itpub13周年纪念徽章
日期:2014-09-28 10:55:55itpub13周年纪念徽章
日期:2014-09-29 01:14:14itpub13周年纪念徽章
日期:2014-10-08 15:15:25itpub13周年纪念徽章
日期:2014-10-08 15:15:25马上有对象
日期:2014-10-12 11:58:40马上有车
日期:2014-11-16 17:11:29
3#
发表于 2015-12-19 16:09 | 只看该作者
沙发,嘿嘿
第二题真难

使用道具 举报

回复
论坛徽章:
10000
地主之星
日期:2015-07-20 17:15:36地主之星
日期:2015-08-31 16:17:58地主之星
日期:2015-09-01 17:59:09地主之星
日期:2015-08-31 16:17:58地主之星
日期:2015-09-01 14:14:25地主之星
日期:2015-08-31 16:17:58地主之星
日期:2015-08-31 16:17:58地主之星
日期:2015-08-31 16:17:58地主之星
日期:2015-08-31 16:17:58地主之星
日期:2015-08-31 16:17:58
4#
发表于 2015-12-19 16:28 | 只看该作者
niubility

使用道具 举报

回复
论坛徽章:
264
布鲁克
日期:2016-10-08 10:06:50秀才
日期:2016-05-20 15:09:32射手座
日期:2016-05-26 14:02:50双子座
日期:2016-05-25 16:05:44白羊座
日期:2016-05-23 11:49:19双鱼座
日期:2016-04-29 17:13:05秀才
日期:2016-04-29 15:03:39秀才
日期:2016-04-29 15:04:10技术图书徽章
日期:2016-04-29 15:04:10秀才
日期:2016-03-28 10:21:13
5#
发表于 2015-12-19 17:00 | 只看该作者
膜拜大神

使用道具 举报

回复
论坛徽章:
740
红钻
日期:2014-12-16 17:51:41布鲁克林篮网
日期:2016-09-23 08:17:18达拉斯小牛
日期:2016-09-23 08:18:15季节之章:冬
日期:2015-07-31 17:16:14ITPUB季度 技术新星
日期:2014-07-17 14:37:00华盛顿奇才
日期:2016-09-23 08:18:15季节之章:夏
日期:2015-07-31 17:16:29绿钻
日期:2015-08-15 13:20:11最佳人气徽章
日期:2015-03-19 09:44:03洛杉矶湖人
日期:2016-09-23 08:18:15
6#
发表于 2015-12-19 21:20 | 只看该作者
膜拜大神

使用道具 举报

回复
论坛徽章:
480
榜眼
日期:2015-09-09 10:34:21秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12秀才
日期:2015-11-23 10:03:12状元
日期:2015-11-23 10:04:09举人
日期:2015-11-23 10:04:09
7#
 楼主| 发表于 2015-12-20 09:30 | 只看该作者
写这个说明很费劲,请多提意见,哪里看不清楚的就讨论个水落石出。

使用道具 举报

回复
招聘 : 系统分析师
论坛徽章:
477
本田
日期:2014-01-05 16:51:44技术图书徽章
日期:2014-04-21 10:26:402014年世界杯参赛球队: 伊朗
日期:2014-05-23 10:41:312014年世界杯参赛球队: 比利时
日期:2014-06-17 12:09:43itpub13周年纪念徽章
日期:2014-09-28 10:55:55itpub13周年纪念徽章
日期:2014-09-29 01:14:14itpub13周年纪念徽章
日期:2014-10-08 15:15:25itpub13周年纪念徽章
日期:2014-10-08 15:15:25马上有对象
日期:2014-10-12 11:58:40马上有车
日期:2014-11-16 17:11:29
8#
发表于 2015-12-20 13:26 | 只看该作者
本帖最后由 lastwinner 于 2015-12-20 13:26 编辑
newkid 发表于 2015-12-20 09:30
写这个说明很费劲,请多提意见,哪里看不清楚的就讨论个水落石出。

所以我把帖子拆分了,要不一大堆的根本没法好好看
希望别介意

使用道具 举报

回复
论坛徽章:
264
布鲁克
日期:2016-10-08 10:06:50秀才
日期:2016-05-20 15:09:32射手座
日期:2016-05-26 14:02:50双子座
日期:2016-05-25 16:05:44白羊座
日期:2016-05-23 11:49:19双鱼座
日期:2016-04-29 17:13:05秀才
日期:2016-04-29 15:03:39秀才
日期:2016-04-29 15:04:10技术图书徽章
日期:2016-04-29 15:04:10秀才
日期:2016-03-28 10:21:13
9#
发表于 2015-12-20 14:54 | 只看该作者
lastwinner 发表于 2015-12-20 13:26
所以我把帖子拆分了,要不一大堆的根本没法好好看
希望别介意

分开好!

使用道具 举报

回复
论坛徽章:
394
阿斯顿马丁
日期:2014-01-03 13:53:522014年世界杯参赛球队:喀麦隆
日期:2014-07-11 12:10:53马上有对象
日期:2014-04-09 16:19:542014年世界杯参赛球队: 洪都拉斯
日期:2014-06-25 08:25:55itpub13周年纪念徽章
日期:2014-09-28 10:55:55itpub13周年纪念徽章
日期:2014-10-01 15:27:22itpub13周年纪念徽章
日期:2014-10-09 12:04:18马上有钱
日期:2014-10-14 21:37:37马上有钱
日期:2015-01-22 00:39:13喜羊羊
日期:2015-02-20 22:26:07
10#
发表于 2015-12-20 20:08 | 只看该作者
第二题初测结果,做出来的同学的执行效率都不太低

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则

TOP技术积分榜 社区积分榜 徽章 团队 统计 知识索引树 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档 |
  | | |
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 网站律师 隐私政策 知识产权声明
 北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表