挑战程序竞赛系列(21):3.2反转

2019-05-26 09:23:52 浏览数 (1)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1434639

挑战程序竞赛系列(21):3.2反转

详细代码可以fork下Github上leetcode项目,不定期更新。

练习题如下:

  • POJ 3276: Face The Right Way
  • POJ 3279: Fliptile
  • POJ 3185: The Water Bowls
  • POJ 1222: Extended Lights Out

POJ 3276: Face The Right Way

解题策略:

每次从最左区间开始,遍历每种可能的k,如k = 3的情况下,开始对每头牛进行翻转,如果最左区间的牛是朝前的,则可以忽略,如果是朝后的,则进行翻转。

策略证明:(从最终态想象,最左端的区间最多只会翻转一次,2次和3次效果和0次1次相同)

第一个版本(模拟):

代码语言:javascript复制
    void solve() {
        int N = ni();
        char[] cow = new char[N];
        for (int i = 0; i < N;   i) cow[i] = nc();
        int k = N;
        int min = 1 << 30;
        int minK = k;
        for (int i = k; i >= 1; --i){
            int cnt = 0;
            char[] clone = Arrays.copyOf(cow, N);

            for (int j = 0; j < N - i   1;   j){
                if(clone[j] == 'F' ) continue;
                for (int l = j; l < i   j;   l){
                    clone[l] = clone[l] == 'F' ? 'B' : 'F';
                }
                cnt  ;
            }
            boolean isValid = true;
            for (int j = N - i   1; j < N;   j){
                if(clone[j] == 'B'){
                    isValid = false;
                    break;
                }
            }
            if (isValid && cnt != 0 && cnt < min){
                min = cnt;
                minK = i;
            }
        }

        out.println(minK   " "   min);
    }

TLE了,时间复杂度为O(n3)O(n^3),模拟是把所有情况走一遍,更客观的原因在于,如果窗口较大,虽然最多只翻转n-k 1次,但一头牛可以翻转多次,模拟把这种翻转多次体现地淋漓尽致。

从数学的角度来看,如果一个区间被翻转奇数次,那么此牛的状态与原状态互异,如果偶数次,此牛状态与原状态相同。所以,k次的翻转是否可以省略了?

按照书上的公式:

∑j=i−K 1i−1fj

sum_{j = i - K 1}^{i-1} fj

如果为奇数,且牛是F,则翻转,如果为偶数,且牛是B,则翻转。

第二版本:

代码语言:javascript复制
    void solve() {
        int N = ni();
        char[] cow = new char[N];
        for (int i = 0; i < N;   i) cow[i] = nc();
        int[] f = new int[N];
        int min = 1 << 30;
        int minK = N;
        for (int k = N; k >= 1; --k){
            f = new int[N];
            for (int i = 0; i < N - k   1;   i){
                int sum = 0;
                for (int j = Math.max(0, i - k   1); j < i;   j){
                    sum  = f[j];
                }
                if ((sum & 1) != 0 && cow[i] == 'F') f[i] = 1;
                else if ((sum % 2) == 0 && cow[i] == 'B') f[i] = 1;
            }
            //检查后续不能翻转,牛的状态
            boolean isValid = true;
            for (int i = N - k   1; i < N;   i){
                int sum = 0;
                for (int j = Math.max(0, i - k   1); j < i;   j){
                    sum  = f[j];
                }
                if ((sum & 1) != 0 && cow[i] == 'F') isValid = false;
                else if ((sum % 2) == 0 && cow[i] == 'B') isValid = false;
            }

            if(isValid){
                int cnt = 0;
                for (int i = 0; i < N;   i){
                    cnt  = f[i];
                }
                if (cnt != 0 && cnt < min){
                    min = cnt;
                    minK = k;
                }
            }
        }
        out.println(minK   " "   min);
    }

呵呵,还是TLE了,虽然没有模拟那么夸张,既要修改牛的状态,又要记录翻转次数,但观察上述代码,它还是有k次循环,内循环中的每个sum是独立的,所以慢。

如果对滑动窗口熟悉的话,它的一个优化就是把窗口大小的sum缓存下来,而每次更新窗口时,它的更新规则为:

∑j=i−K 1 1ifj=∑j=i−K 1i−1fj fi−fi−K 1

sum_{j = i - K 1 1}^{i} fj = sum_{j = i - K 1}^{i-1}fj fi - fi - K 1

可以理解成累加和,或者缓存,或者空间换时间,whatever,但需要知道的是滑动窗口移动时,k-1元素的关联性可以有效利用起来。

第三版本:

代码语言:javascript复制
void solve() {
        int N = ni();
        char[] cow = new char[N];
        for (int i = 0; i < N;   i) cow[i] = nc();
        int[] f = new int[N];
        int min = 1 << 30;
        int minK = N;
        for (int k = N; k >= 1; --k){
            f = new int[N];
            int sum = 0;
            for (int i = 0; i < N - k   1;   i){
                if ((sum & 1) != 0 && cow[i] == 'F') f[i] = 1;
                else if ((sum % 2) == 0 && cow[i] == 'B') f[i] = 1;
                sum  = f[i];
                if (i - k   1 >= 0){
                    sum -= f[i - k   1];
                }
            }
            boolean isValid = true;
            for (int i = N - k   1; i < N;   i){
                if ((sum & 1) != 0 && cow[i] == 'F') isValid = false;
                else if ((sum % 2) == 0 && cow[i] == 'B') isValid = false;
                if (i - k   1 >= 0){
                    sum -= f[i - k   1];
                }
            }

            if(isValid){
                int cnt = 0;
                for (int i = 0; i < N;   i){
                    cnt  = f[i];
                }
                if (cnt != 0 && cnt < min){
                    min = cnt;
                    minK = k;
                }
            }
        }
        out.println(minK   " "   min);
    }

POJ 3279: Fliptile

书上的思路讲的很清楚,因为每一格最多只会翻一次,所以翻过的就不会再翻了,这点很关键。

一般的做法是遍历所有格子,选择翻or不翻,复杂度为O(2MN)O(2^{MN}),考虑到每个格子的关联性,可以降低复杂度,比如,确定了第一行的排列,总共有2N2^N次种翻法。

那么由于第一行翻过的就不会再动,那么第一行中还存在黑色格子的情况下,只能选择第二行的某个格子去除,这是一种一一对应关系,连带着把后面所有的情况都确定了。所以复杂度就只有O(MN2MN)O(MN2^{MN}).

上述性质很难证明,此类题难道只能靠猜?

代码如下:(模拟)

代码语言:javascript复制
    int[][] dir = {{0,0},{1,0},{-1,0},{0,1},{0,-1}};
    void solve() {
        int M = ni();
        int N = ni();
        int[][] board = new int[M][N];
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                board[i][j] = ni();
            }
        }

        int[][] click = new int[M][N];

        int min = 1 << 30;
        int[][] ans = new int[M][N];
        for (int i = 0; i < 1 << N;   i){
            click = new int[M][N];
            int[][] clone = clone(board);
            for (int j = 0; j < N;   j){
                click[0][N - 1 - j] = ((i & (1 << j)) != 0) ? 1 : 0;
            }

            flip(clone, click, 0);
            for (int j = 1; j < M;   j){
                for (int k = 0; k < N;   k){
                    click[j][k] = clone[j-1][k];
                }
                flip(clone, click, j);
            }

            if(!valid(clone)) continue;

            int cnt = cal(click);
            if (cnt != 0 && cnt < min){
                min = cnt;
                ans = clone(click);
            }
        }

        if (min == 1 << 30){
            out.println("IMPOSSIBLE");
            return;
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                sb.append(ans[i][j]   ((j   1 != N) ? " " : "n"));
            }
        }
        out.println(sb.toString());
    }

    public int cal(int[][] click){
        int M = click.length;
        int N = click[0].length;
        int cnt = 0;
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                cnt  = click[i][j];
            }
        }
        return cnt;
    }

    public boolean valid(int[][] board){
        int M = board.length;
        int N = board[0].length;
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                if (board[i][j] != 0) return false;
            }
        }
        return true;
    }

    public void flip(int[][] board, int[][] click, int row){
        int N = click[0].length;
        int M = click.length;
        for (int i = 0; i < N;   i){
            if (click[row][i] == 1){
                for (int[] d : dir){
                    int x = d[0]   row;
                    int y = d[1]   i;
                    if (x >= 0 && x < M && y >= 0 & y < N){
                        board[x][y] = board[x][y] == 1 ? 0 : 1;
                    }
                }
            }
        }
    }

    public int[][] clone(int[][] board){
        int M = board.length;
        int N = board[0].length;
        int[][] clone = new int[M][N];
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                clone[i][j] = board[i][j];
            }
        }
        return clone;
    }

POJ 3185: The Water Bowls

苦苦折腾了一个小时。。。至今不知道之前的做法哪里错了,蛋疼。头尾可以翻转2个碗,中间的必须翻转3个碗,这是此题唯一的一个trick。想法是,把20扩充到21,这样当第一个元素为1时,相当于在头部翻转2个碗。

代码如下:

代码语言:javascript复制
int MAX_N = 20   1;
    int[] dir = new int[MAX_N];
    int[] f = new int[MAX_N];

    void solve() {
        for (int i = 1; i < 21;   i) dir[i] = ni();
        out.println(Math.min(calc(0), calc(1)));
    }

    int calc(int fir){
        int N = MAX_N;
        f = new int[MAX_N];
        dir[0] = fir;
        int res = 0;
        int sum = 0;
        for (int i = 0; i < N - 1;   i){
            if (((dir[i]   sum) & 1) != 0){
                  res;
                f[i] = 1;
            }

            sum  = f[i];
            if (i - 2 >= 0) sum -= f[i -2];
        }

        if (((dir[N-1]   sum) & 1) != 0){
            return 1 << 30;
        }

        return res;
    }

POJ 1222: Extended Lights Out

好气啊,和第二题基本一个解题模式,但此题不需要输出最小的步数,直接枚举出一种答案即可?搞不懂,为什么加个最小步数,就WA了。

代码如下:

代码语言:javascript复制
void solve() {
        int n = ni();
        for (int k = 1; k <= n;   k){
            int M = 5;
            int N = 6;
            int[][] board = new int[M][N];
            for (int i = 0; i < M;   i){
                for (int j = 0; j < N;   j){
                    board[i][j] = ni();
                }
            }
            out.println("PUZZLE #"   k);
            out.println(calc(board, M, N));
        }
    }

    int[][] dir = {{0,0},{1,0},{-1,0},{0,1},{0,-1}};
    String calc(int[][] board, int M, int N) {

        int[][] click = new int[M][N];

        int[][] ans = new int[M][N];
        for (int i = 0; i < 1 << N;   i){
            click = new int[M][N];
            int[][] clone = clone(board);
            for (int j = 0; j < N;   j){
                click[0][N - 1 - j] = ((i & (1 << j)) != 0) ? 1 : 0;
            }

            flip(clone, click, 0);
            for (int j = 1; j < M;   j){
                for (int k = 0; k < N;   k){
                    click[j][k] = clone[j-1][k];
                }
                flip(clone, click, j);
            }

            if(!valid(clone)) continue;

            ans = clone(click);
            break;
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                sb.append(ans[i][j]   ((j   1 != N) ? " " : "n"));
            }
        }
        return sb.toString().substring(0, sb.length() - 1);
    }

    public int cal(int[][] click){
        int M = click.length;
        int N = click[0].length;
        int cnt = 0;
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                cnt  = click[i][j];
            }
        }
        return cnt;
    }

    public boolean valid(int[][] board){
        int M = board.length;
        int N = board[0].length;
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                if (board[i][j] != 0) return false;
            }
        }
        return true;
    }

    public void flip(int[][] board, int[][] click, int row){
        int N = click[0].length;
        int M = click.length;
        for (int i = 0; i < N;   i){
            if (click[row][i] == 1){
                for (int[] d : dir){
                    int x = d[0]   row;
                    int y = d[1]   i;
                    if (x >= 0 && x < M && y >= 0 & y < N){
                        board[x][y] = board[x][y] == 1 ? 0 : 1;
                    }
                }
            }
        }
    }

    public int[][] clone(int[][] board){
        int M = board.length;
        int N = board[0].length;
        int[][] clone = new int[M][N];
        for (int i = 0; i < M;   i){
            for (int j = 0; j < N;   j){
                clone[i][j] = board[i][j];
            }
        }
        return clone;
    }
sum

0 人点赞