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

来源:互联网 发布:淘宝上的satya精油好吗 编辑:程序博客网 时间:2024/05/23 19:54

挑战程序竞赛系列(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次相同)

第一个版本(模拟):

    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),模拟是把所有情况走一遍,更客观的原因在于,如果窗口较大,虽然最多只翻转n-k+1次,但一头牛可以翻转多次,模拟把这种翻转多次体现地淋漓尽致。

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

按照书上的公式:

j=iK+1i1f[j]

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

第二版本:

    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=iK+1+1if[j]=j=iK+1i1f[j]+f[i]f[iK+1]

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

第三版本:

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),考虑到每个格子的关联性,可以降低复杂度,比如,确定了第一行的排列,总共有2N次种翻法。

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

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

代码如下:(模拟)

    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个碗。

代码如下:

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了。

代码如下:

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;    }