2.1穷尽搜索

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

练习题如下:
1. POJ 2718: Smallest Difference
2. POJ 3187: Backward Digits Sums
3. POJ 3050: Hopscotch
4. AOJ 0525: Osenbei

POJ 2718: Smallest Difference

一个数切一刀拆成两个数,两个数每一位数字的顺序都可改变,但是不能有前导0。求这两个数之差的最小值。

思路:
尽量拆分成长度相等的两个数,最小值一定存在这种情况下,得到两个数之后,在原来的数上求出所有可能的顺序,用到的知识点为nextPermutation().有了所有的顺序,我们穷尽所有顺序求得答案即可,代码如下:

public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int N = Integer.parseInt(in.nextLine());
        while(N-- != 0){
            String[] nums = in.nextLine().trim().split(" ");
            int[] digits = new int[nums.length];
            for(int i = 0; i <nums.length; i++){
                digits[i] = nums[i].charAt(0)-'0';
            }
            System.out.println(solve(digits));
        }
        in.close();
    }

    private static int solve(int[] digits){
        int n = digits.length;
        int size1 = n / 2;
        int size2 = size1;
        if (n % 2 != 0){
            size2 ++;
        }
        int min = Integer.MAX_VALUE;
        do{
            int k = 0;
            String a1 = "";
            for (int i = 0; i < size1; i++){
                a1 += digits[k++];
            }
            String a2 = "";
            for (int i = 0; i < size2; i++){
                a2 += digits[k++];
            }
            int ans = Math.abs(Integer.parseInt(a1)-Integer.parseInt(a2));
            min = Math.min(min, ans);
        }while(nextPermutation(digits) != null);
        return min;
    }


    public static int[] nextPermutation(int[] nums){
        int n = nums.length;
        int pos = n - 2;
        while (pos >= 0 && nums[pos] - nums[pos+1] >= 0){
            pos --;
        }
        if (pos == -1) return null;

        int j = n - 1;
        while (j > pos && nums[j] <= nums[pos]){
            j --;
        }
        swap(nums, pos, j);
        reverse(nums, pos+1, n-1);
        return nums;
    }

    private static void swap(int[] nums, int x, int y){
        int tmp = nums[x];
        nums[x] = nums[y];
        nums[y] = tmp;
    }

    private static void reverse(int[] nums, int s, int e){
        while (s < e){
            swap(nums, s, e);
            s++;
            e--;
        }
    }

为了得到所有的顺序,我们必须得遍历所有可能的顺序,而这种情况会有 O(n!) 种情况,这复杂度实在太高了,果断TLE。

优化思路:
它其实是一道排列组合题,我不知道网上说的贪心是什么解法,暂且没有想明白贪心的做法。在上述思路中,我们知道,如果n为偶数,那就意味着,把数放在box1和box2中的任何一个是顺序无关的。所以在暴力nextPermutation时,我们可以分成n/2的两个list,分别进行nextPermutation,现在要做的无非就是从n个数中选择n/2个数装进box1,剩余的数装进box2。

在JAVA中,我能想到的随机选择n/2个数的方法是遍历 2n 次,如:

n = 4
初始化:
1 << 4; 16
二进制表示:
10000 
遍历:while (16-- != 0)
1111
1110
.
.
.
0010
0001
0000

01个数相等的位筛选出来,就是从n个数中随机选择n/2个数的所有情况。

详细代码如下:

private static int solve(int[] digits){
        int n = digits.length;
        int permutate = 1 << n;
        int cut = n / 2;
        int min = Integer.MAX_VALUE;
        while (permutate-- != 0){
            String bitSet = Integer.toBinaryString(permutate);
            int[] list1 = new int[cut];
            int[] list2 = new int[n-cut];

            char[] bits = bitSet.toCharArray();
            String a1 = "";
            String a2 = "";
            int k = 0;
            for (int i = bits.length-1; i >= 0; i--){
                if (bits[i] == '1'){
                    a1 += digits[k++];
                }else{
                    a2 += digits[k++];
                }
            }
            while (k < n){
                a2 += digits[k++];
            }

            if (a1.length() != cut){
                continue;
            }

            if (a1.charAt(0) == '0' && a1.length() > 1){
                continue;
            }

            for (int i = 0; i < list1.length; i++){
                list1[i] = a1.charAt(i)-'0';
            }

            for (int i = 0; i < list2.length; i++){
                list2[i] = a2.charAt(i)-'0';
            }

            int[] c1 = clone(list1);
            int[] c2 = clone(list2);
            do{
                String x1 = "";
                for (int i = 0; i < list1.length; i++){
                    x1 += c1[i];
                }
                c2 = clone(list2);
                do{
                    String x2 = "";
                    for (int i = 0; i < list2.length; i++){
                        x2 += c2[i];
                    }
                    if (x2.charAt(0) == '0' && x2.length() > 1) continue;
                    int diff = Math.abs(Integer.parseInt(x1)-Integer.parseInt(x2));
                    min = Math.min(min, diff);
                }while(nextPermutation(c2) != null);
            }while (nextPermutation(c1) != null);
        }
        return min;
    }

POJ 3187: Backward Digits Sums

思路:
非常简单,遍历所有可能的顺序,依次求出sum即可,符合就返回结果。sum的求解使用了递归,每次递归,样本减一,所以时间复杂度为 O(n) 。代码如下:

public class SolutionDay19_P3187 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int N = in.nextInt();
        int S = in.nextInt();
        solve(N, S);
        in.close();
    }

    private static void solve(int N, int S){
        int[] nums = new int[N];
        for (int i = 0; i < N; i++){
            nums[i] = i + 1;
        }
        do{
            if (S == sum(nums)) break;
        }while(nextPermutation(nums) != null);

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < nums.length; i++) {
            sb.append(nums[i] + " ");
        }
        System.out.println(sb.toString().substring(0,sb.length()-1));
    }

    private static int sum(int[] nums){
        if (nums.length == 1) return nums[0];
        int[] sum = new int[nums.length-1];
        for (int i = 0; i < sum.length; i++){
            sum[i] = nums[i] + nums[i+1];
        }
        return sum(sum);
    }

    public static int[] nextPermutation(int[] nums){
        int n = nums.length;
        int pos = n - 2;
        while (pos >= 0 && nums[pos] - nums[pos+1] >= 0){
            pos --;
        }
        if (pos == -1) return null;

        int j = n - 1;
        while (j > pos && nums[j] <= nums[pos]){
            j --;
        }
        swap(nums, pos, j);
        reverse(nums, pos+1, n-1);
        return nums;
    }

    private static void swap(int[] nums, int x, int y){
        int tmp = nums[x];
        nums[x] = nums[y];
        nums[y] = tmp;
    }

    private static void reverse(int[] nums, int s, int e){
        while (s < e){
            swap(nums, s, e);
            s++;
            e--;
        }
    }

}

POJ 3050: Hopscotch

思路:
依旧是遍历,用Set记录非重复的元素,采用DFS+回溯,其他的没什么。代码如下:

public class SolutionDay19_P3050 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int[][] grid = new int[5][5];
        for (int i = 0; i < 5; i++){
            for (int j = 0; j < 5; j++){
                grid[i][j] = in.nextInt();
            }
        }
        System.out.println(solve(grid));
        in.close();
    }

    private static int solve(int[][] grid){
        ans = new HashSet<>();
        for (int i = 0; i < 5; i++){
            for (int j = 0; j < 5; j++){
                dfs(grid,i, j, 5, "");
            }
        }
        return ans.size();
    }

    static Set<String> ans = new HashSet<>();
    static int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};

    private static void dfs(int[][] grid,int i, int j, int hop,String path){
        path += grid[i][j];
        if (hop == 0){
            ans.add(path);
        }else{
            for (int[] d : dir){
                int nx = i + d[0];
                int ny = j + d[1];
                if (nx >= 0 && nx < 5 && ny >= 0 && ny < 5){
                    dfs(grid, nx, ny, hop-1, path);
                }
            }
        }
        path.substring(0,path.length()-1);
    }

}

AOJ 0525: Osenbei

翻译参考博文【AOJ 0525 Osenbei《挑战程序设计竞赛(第2版)》练习题答案

题意:药药!切克闹! 煎饼果子来一套!有一个烤饼器可以烤r行c列的煎饼,煎饼可以正面朝上(用1表示)也可以背面朝上(用0表示)。一次可将同一行或同一列的煎饼全部翻转。现在需要把尽可能多的煎饼翻成正面朝上,问最多能使多少煎饼正面朝上?
输入:多组输入,每组第一行为二整数r, c (1 ≤ r ≤ 10, 1 ≤ c ≤ 10 000),剩下r行c列表示煎饼初始状态。r=c=0表示输入结束。
输出:对于每组输入,输出最多能使多少煎饼正面朝上。

这道题很有意思,乍一看,它的选择千变万化,不知道如何下手,实则不然。

思路:
关于翻转有一个特性:翻转两次就能覆盖所有结果,翻转第三次时与第一次的结果相同,翻转第四次与第二次的结果相同。所以这道题,每行选择翻或者不翻,总共就有 O(2row) 种情况,而一旦每一行确定后,关于每一列,只需要选择{0,1}中个数较多的那个数进行累加即可。

代码如下:

public class SolutionDay19_A0525 {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (true) {
            int row = in.nextInt();
            int col = in.nextInt();
            if (row == 0 && col == 0)
                break;

            int[][] grid = new int[row][col];
            for (int i = 0; i < row; i++) {
                for (int j = 0; j < col; j++) {
                    grid[i][j] = in.nextInt();
                }
            }
            System.out.println(solve(grid));
        }
        in.close();
    }

    private static void flip(int[][] grid, int row){
        for (int i = 0; i < grid[0].length; i++){
            grid[row][i] = grid[row][i] == 1 ? 0 : 1;
        }
    }

    private static int total(int[][] grid, int col){
        int n = grid.length;
        int count = 0;
        for (int i = 0; i < n; i++){
            if (grid[i][col] == 1) count ++;
        }
        return Math.max(count, n - count);
    }

    private static int solve(int[][] grid) {
        int n = grid.length;
        int m = grid[0].length;

        int nn = 1 << n;
        int max = Integer.MIN_VALUE;
        while (nn-- != 0) {
            String nns = Integer.toBinaryString(nn);
            char[] nnc = nns.toCharArray();
            for (int i = 0; i < nnc.length; i++){
                if (nnc[i] == '1'){
                    flip(grid, i);
                }
            }
            int count = 0;
            for (int j = 0; j < m; j++){
                count += total(grid, j);
            }
            max = Math.max(max, count);

            //复原
            for (int i = 0; i < nnc.length; i++){
                if (nnc[i] == '1'){
                    flip(grid, i);
                }
            }
        }
        return max;
    }
}

关于位置的随机选取,不一定要使用String的方法,而是直接使用位操作,代码如下:

private static int solve(int[][] grid) {
        int n = grid.length;
        int m = grid[0].length;

        int nn = 1 << n;
        int max = Integer.MIN_VALUE;

        for (int i = 0; i < nn; i++){
            for (int j = 0; j < n; j++){
                if ((i & (1 << j)) != 0) {
                    flip(grid, j);
                }
            }
            int count = 0;
            for (int j = 0; j < m; j++) {
                count += total(grid, j);
            }
            max = Math.max(max, count);
            for (int j = 0; j < n; j++){
                if ((i & (1 << j)) != 0) {
                    flip(grid, j);
                }
            }
        }
        return max;
    }
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐