分治算法实例——Gold Miner

题目描述

Nezzar is addicted to game Gold Miner recently.

Gold Miner is a game in which the player is in an n×n grid, and the goal is to collect gold without entering forbidden area. It is known that cell (i,j)(i,j) is forbidden to enter if and only if i=j or i=j+1.

In one round, Nezzar may choose two non-empty sets X,Y⊆[n]X,Y⊆[n], and collect gold in cell (i,j)(i,j) for all i∈X,j∈Yi∈X,j∈Y. However, he immediately fails if there exists i∈Xi∈X and j∈Yj∈Y such that (i,j)(i,j) is forbidden to enter.

As a greedy miner, Nezzar would like to enter all but forbidden cells to collect gold. However, he is running out of time! He can only play the game for at most 50 rounds. Can you design an algorithm to solve this problem for him?

Input

One line containing one integer n (2≤n≤3000), the size of the grid.

Output

Please read this part carefully, as you may fail to pass the test cases if you have formating issues.
For each round with subsets X,Y being chosen, output elements in X increasingly in the first line, and then output elements in Y increasingly in the second line.

Sample Input

3

Sample Output

1
2 3
1 2
3
3
1

Note

It is allowed to enter some cells multiple times!

Subtasks

  • (5 points) n≤7n≤7
  • (15 points) n≤50n≤50
  • (10 points) n≤100n≤100
  • (30 points) n≤1000n≤1000
  • (40 points) n≤3000n≤3000

Hint

Believe it or not, we can use divide-and-conquer technique to solve this task.
Let T(n) denote the number of rounds we need to solve the above task with parameter n. Notice that T(n)=T(n/2)+O(1) implies T(n)=O(logn)T(n)=O(log⁡n), and we may need an algorithm satisfying this recurrence to solve the problem.

题目理解

在一个n×n的方格中,一些区域(x=y or x=y+1)设置为forbidden,每一个round如果碰到forbidden就fail。
每一个round,输入是X,Y的集合,当前round把空白方格填上颜色,每一次填上的是一块类矩形的区域(x,y的所有组合)

在这里插入图片描述

心路历程

首先是想要人为地去模拟看每一种有什么画法,并且探索了从n=5开始每一种都至少需要4步,但是具体拿到一个数字没有办法很快地得出具体该怎么画,而且相同步数也会对应有不同的画法,总之,未找到通解。

第二条线是根据hint,divide and conquer 的思想想要探索T(n)和T(n/2)的关系,首先想到的是,如果会画 n/2 的情形,那么在左上角时,可以将后半截的y加上,因为x总会遍历完所有的行,所以右上角的空白能被完全填上,下半部分也行得通。问题是这样的划分只能得到T(n) = 2T(n/2)+O(1),由之前学过的知识T(n) = aT(n/b)+O(1),只有a比b小的时候分治法才会有较好的结果。所以pass。

最终的解决终究还是用到了分治法,如果知道了T(n/2)的图怎么填齐,那么可以操纵画笔同时在四块区域进行填色,剩下的空白可4步完成(如果n为奇数,需要5步),最后终于有思路啦啦啦~

题解

假设已经知道T(n/2)的画法,同时填齐,但不要图右上角,所以每一个n的最后一步必须是x=1的时候来填,这样才好在下一次用到时实现不填。

黄色表示不能填的地方,绿色表示由T(n/2)的填法,来填色的部分,红框表示最后几步。

图1 n为偶数
图2 n为奇数

代码

整体需要的外部类,不太重要。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

定义了一个Step类,采用了Arraylist来用来记录每一个步骤,并实现添加,打印等方法。

class Step{
    private ArrayList<Integer> x;
    private ArrayList<Integer> y;
    //一系列构造函数的重载,当时估计是为了方便new。。。
    Step(){
        x = new ArrayList<Integer>();
        y = new ArrayList<Integer>();
    }
    Step(int[] ar, int[] br){
        x = new ArrayList<Integer>();
        y = new ArrayList<Integer>();
        for(int a: ar){
            x.add(a);
        }
        for(int b:br){
            y.add(b);
        }
    }
    Step(int a, int b){
        x = new ArrayList<Integer>();
        y = new ArrayList<Integer>();
        x.add(a);
        y.add(b);
    }
    
    //属性为private,通过public的接口来赋值,算是防御式编程吧
    public void addX(int a){
        this.x.add(a);
    }
    public void addY(int a){
        this.y.add(a);
    }
    public ArrayList<Integer> getX(){
        return this.x;
    }
    public ArrayList<Integer> getY(){
        return this.y;
    }

    /*
    @param  int m 对应是那个方形边长的1/2,即是我们要扩展的增加的长度
    @param  boolean flag 是否是偶数情况
    @return  将step实现从T(n/2)到T(n)的扩展第一步,对应图上绿色部分
    */
    public void expand(int m, boolean flag){
        int l = x.size();
        int tem;
        if(flag){//偶数情况
            for(int i=0; i<l; i++){
                tem = x.get(i);
                x.add(tem+m);
            }
            l = y.size();
            for(int i=0; i<l; i++){
                tem = y.get(i);
                y.add(tem+m);
            }
        }else{//奇数情况
            for(int i=0; i<l; i++){
                tem = x.get(0);
                x.remove(0);
                x.add(tem+1);
                x.add(tem+m+1);
            }
            l = y.size();
            for(int i=0; i<l; i++){
                tem = y.get(0);
                y.remove(0);
                y.add(tem+1);
                y.add(tem+m+1);
            }
        }
    }
    
    //一个步骤的格式化打印
    public void iPrint(){
        int l = x.size();
        if(l>0){
            System.out.print(x.get(0));
            for(int i=1; i<l; i++){
                System.out.print(" "+x.get(i));
            }
        }
        System.out.println();
        l = y.size();
        if(l>0){
            System.out.print(y.get(0));
            for(int i=1; i<l; i++){
                System.out.print(" "+y.get(i));
            }
            System.out.println();
        }
    }
}

GoldMIner类

public class GoldMiner {
    private void printHelper(ArrayList<Step> T){
        int l = T.size();
        Step step;
        for(int i=0; i<l; i++){
            step = T.get(i);
            step.iPrint();
        }
    }
    
    
    //相当于一个接口,因为要递归调用,所以实现算法(不包括打印的部分)要抽象成一个函数。
    public void solve(int n){
        ArrayList<Step> T = new ArrayList<Step>();
        reSolve(n,T);
        printHelper(T);
    }

	
    public ArrayList<Step> reSolve(int n, ArrayList<Step> T){
        int l;
        Step step;
        ArrayList<Integer> xy;
        assert n>=2;
        //先解决递归的base
        if(n == 2){
            T.add(new Step(1,2));
        }else if(n == 3){
            T.add(new Step(new int[]{1}, new int[]{2,3}));
            T.add(new Step(2,3));
            T.add(new Step(3,1));
        }else {//复杂的来了
            boolean flag = n%2==0;
            n /= 2;
            T = reSolve(n,T);//先解决T(n/2)
            l = T.size();
            step = T.get(0);
            ArrayList<Integer> arr = step.getY();
            if(arr.size()==1){
                T.remove(l-1);//如果最后一笔,只填了一笔就是最左上角那个格子,那么扩展时,这一步删去即可
                l--;
            }else{//否则就是把,最后这一笔的那个最右上角的格子从这一步中去掉,然后扩展
                arr.remove(arr.size()-1);
                step.expand(n,flag);
            }

            for(int i=1; i<l; i++){
                step = T.get(i);
                step.expand(n,flag);
            }//用expand完成T(n/2)到T(n)的大部分,即绿色的地方。
            
            if(flag){//偶数
                step = new Step();
                step.addY(1);
                for(int i=2; i<=n; i++){
                    step.addX(i+n);
                    step.addY(i);
                }
                T.add(0,step);
                step = new Step(n+1,1);
                step.addY(2*n);
                T.add(0,step);
                step = new Step();
                step.addY(1+n);
                for(int i=2; i<=n; i++){
                    step.addX(i);
                    step.addY(i+n);
                }
                T.add(0,step);
                step = new Step(1,n);
                for(int i=1; i<=n; i++){
                    step.addY(n+i);
                }
            }else{//奇数
                n = n + 1;
                step = new Step();
                step.addY(1);
                step.addY(2);
                for (int i = 3; i <= n; i++) {
                    step.addX(n + i - 1);
                    step.addY(i);
                }
                T.add(0,step);
                step = new Step(n + 1, 1);
                step.addY(2);
                step.addY(2 * n - 1);
                T.add(0,step);
                step = new Step();
                step.addY(1);
                step.addY(n + 1);
                for (int i = 3; i <= n; i++) {
                    step.addX(i);
                    step.addY(n + i - 1);
                }
                T.add(0,step);
                step = new Step(2, n);
                for (int i = 1; i < n; i++) {
                    step.addY(n + i);
                }
                T.add(0,step);
                step = new Step(1, 2);
                for (int i = 3; i < 2*n; i++) {
                    step.addY(i);
                }
            }
            T.add(0,step);
        }//有一个细节是把最后几步插在最前面,因为这样的话下一次可以直接取0,就能获得填右上角那个格子的一步。
        return T;
    }


    public static void main(String[] args){
        GoldMiner g = new GoldMiner();
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try{
            int n = Integer.parseInt(br.readLine());
            g.solve(n);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

终于完了,呼

在数据的存储上或许还有改进,之后有时间再看吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值