第五届新疆省ACM-ICPC程序设计竞赛(Java版)

比赛链接

狂赌之渊

题目描述

有 n 堆石头,第 i 堆石头有 ai个石子,两人轮流操作,每次操作先选择一堆石头,再从这堆石头中取走一个石子,如果此次操作取完了被选择的这堆石头的最后一个石子,操作者得一分。当所有石子被取走时,游戏结束。输出先手最大得分。

输入描述

第一行一个整数n(0<n<1e5),第二行 n 个整数,第 i 个数字表示 ai(2<=ai<=1e9).

输出描述

输出一个整数表示先手最大得分。

算法分析

博弈问题:
1)对于一堆石子具有偶数个,如果先手先选择,则后手拿最后一个,对于一堆石子具有奇数个,如果先手先选择,则先手拿最后一个。
2)因此二人为了自己得分更高(都想拿最后一个石子),因此都会优先抢占奇数堆的先手(即抢拿奇数堆的第一个石子)。
3)因此当第一个人抢了第一个奇数堆的第一个石子后,第二个人,也会去抢其他奇数堆的第一个石子,直到所有堆都剩下偶数个石子。
4)由此可见谁拿了最后一个奇数堆的第一个石子,谁就可以全胜,因为,之后的另一个人,必是每一个偶数堆的先手,因此必全败。

所以只需统计奇数堆的个数,如果是奇数个,则先手全胜,即n分,反之全败,即0分。

代码展示

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] x = new int[n];
        int odd = 0;
        for (int i = 0; i < n; i++) {
            x[i] = sc.nextInt();
            if ((x[i] & 1)==1)
                odd++;
        }
        if ((odd & 1)==1)
            System.out.println(n);
        else
            System.out.println(0);
    }
}

最长递增长度

题目描述

给定一个长度为n的整数序列S,求这个序列中最长的严格递增子序列的长度。

输入描述

第一行,一个整数n (2<=n<=50000),表示序列的长度

第二行,有n个整数 (-10^9 <= S[i] <= 10^9),表示这个序列

输出描述

输出一个整数,表示最长递增子序列的长度

算法分析

最长递增子序列,动态规划中的经典例题,对于该题数值较大,如果使用经典代码,则运行超时。

代码展示

经典算法(超时)

import java.util.Scanner;
 
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] x = new int[n];
        
        for (int i = 0; i < n; i++) {
            x[i] = sc.nextInt();
        }
        // dp[i]:表示以第i个元素结尾的最长递增子序列
        int[] dp = new int [n];
        int ans = 0;
        for (int i = 0; i < n; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (x[i] > x[j]) {
                    dp[i] = Math.max(dp[i], dp[j]+1);
                }
            }
            ans = Math.max(dp[i], ans);
        }
        System.out.println(ans);
    }
}

正确解法维护最大递增数组

我们可以维护一个最小的不断递增的数组,每一个往数组的插入数据都插入到第一个比它的数的位置,也就是把第一个比它大的数替换了,这样便于后来增加递增数组的长度
比如:4 0 5 8 7 8 的替换过程
1)4
2)0
3)0 5
4)0 5 8
5)0 5 7
6)0 5 7 8

因此,我们的关键就是在递增的有序数组里面找到第一个大于待插数的下标,二分查找便是最佳选择。

手写二分法

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] x = new int[n];

        for (int i = 0; i < n; i++) {
            x[i] = sc.nextInt();
        }
       // 维护一个最大的递增数组
        int[] y = new int[n];
        int len= 0;
        for (int i = 0; i < n; i++) {
            // 二分查找第一个大于x[i]的位置,返回 -insertindex-1,如果没有找到则是toindex(from-toindex左闭右开)即寻找范围的末尾
            int index = binarySearch(y, len, x[i]);
            y[index] = x[i];
            // 如果插入位置是递增数组最后一个,插入后递增数组长度 len++
            if (index == len)
                len++;
        }
        System.out.println(len);
    }
    // 二分搜索
    public static int binarySearch(int[] x, int len, int key) {
        int l = 0, r = len;
        while (l < r) {
            int mid = (l+len) >>> 1;
            if (x[mid] >= key)
                r = mid;
            else
                l = mid+1;
        }
        return l;
    }
}

Arrays.binarySearch()

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] x = new int[n];
        
        for (int i = 0; i < n; i++) {
            x[i] = sc.nextInt();
        }
        // 维护一个最大的递增数组
        int[] y = new int[n];
        int len = 0;
        
        for (int i = 0; i < n; i++) {
            // 二分查找第一个大于x[i]的位置,返回 -insertindex-1,如果没有找到则是toindex(from-toindex左闭右开)即寻找范围的末尾
            int index = Arrays.binarySearch(y, 0, len, x[i]);
            index = -(index+1);  // 插入位置
            y[index] = x[i];
            
            // 如果插入位置是递增数组最后一个,插入后++
            if (index == len)
                len++;
        }
        System.out.println(len);
    }
}

Arrays.binarySearch() 使用介绍:

源码展示

// Like public version, but without range checks.
    private static int binarySearch0(int[] a, int fromIndex, int toIndex,
                                     int key) {
        int low = fromIndex;
        int high = toIndex - 1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            int midVal = a[mid];

            if (midVal < key)
                low = mid + 1;
            else if (midVal > key)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }

分析源码:当未找到 key 时返回的是 -(low + 1),而 -(返回值+1) 便是第一个大于 key 的数的下标

官方API

public static int binarySearch​(int[] a,
int fromIndex,
int toIndex,
int key)

使用二叉搜索算法搜索指定值的指定数组的范围。 在进行此呼叫之前,范围必须按照sort(int[], int, int) 方法进行排序。 如果没有排序,结果是未定义的。 如果范围包含具有指定值的多个元素,则不能保证将找到哪个元素。
参数
a - 要搜索的数组
fromIndex - 要搜索的第一个元素(包括)的索引
toIndex - 要搜索的最后一个元素(排他)的索引
key - 要搜索的值
结果
搜索键的索引,如果它包含在指定范围内的数组中; 否则, (-(insertion point) - 1) 。 插入点被定义为键被插入到数组中的点:第一个元素在大于键的范围内的索引,如果范围中的所有元素都小于指定的键, toIndex
请注意,这确保当且仅当找到该键时返回值将为 >= 0。

大吉大利

题目描述

有 n 个人,编号为 1 ~ n,第 i 个人有 a[i] 枚金币,若第一个人金币数大于 0,则可以选择一个 i (2 ≤ i ≤ n) 然后,弃置 1 枚金币,让第 i 个人弃置 b[i] 枚金币,若第 i 个人金币数少于 b[i] 则弃置所有金币。现需要让第 1 个人弃置最少的金币,成为唯⼀的金币数最多的人。

输入描述

第一行一个正整数 n(n≤100000),第二行 n 个正整数 ai (1≤a i ​ ≤100000),第三行,n-1 个正整数,第 i 个表示 bi+1 ​(1≤b i+1 ​ ≤100000)。

输出描述

输入自己需要弃置的最少金币,如果无解输出 -1。

算法分析

暴力遍历,当a[i] >= a[0] 时,那就直接让a[i] 花费1金币,让 a[i] - b[i],当 a[0] = 0 时,输入 -1.

代码展示

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] x = new int[n];
        int[] y = new int[n];
        for (int i = 0; i < n; i++) {
            x[i] = sc.nextInt();
        }
        for (int i = 1; i < n; i++) {
            y[i] = sc.nextInt();
        }
        boolean flag = true;
        int sum = 0;
        for (int i = 1; i < n; i++) {
            while (x[i] >= x[0]) {
                if (x[0] == 0) {
                    flag = false;
                    break;
                }
                x[i] -= y[i];
                x[0]--;
                sum++;
            }
        }
        if (!flag)
            System.out.println(-1);
        else
            System.out.println(sum);
    }
}

虚无的后缀

题目描述

给出 n 个数字,第 i 个数字为 a[i],我们从中选出 k 个数字,使得乘积后缀 0 的个数最多。

输入描述

第一行,两个正整数 n,k(1≤k≤n≤200),第 2 行 n 个正整数表示

输出描述

输出一个整数,表示最多有多少个后缀 0

算法分析

参考上篇博客 阶乘后的零

比如:10,因此一个2一个5,最终一个0。0的个数,与因子2与5的个数有关。
对于求0的个数,我们必然需要选择K个数,使得这K个数,相乘最终的因子2与5的个数最小值,想对于其他选择最多,即0最多。

动态规划

01背包问题的拓展

  • 首先我们需要统计每一个数的 因子2与因子5的个数
    • c2[i]:代表第i个数因子2的个数
    • c5[i]:代表第i个数因子5的个数
  • 另外需要统计因子5的总个数:max(类似背包问题的背包容量)
  • 创建 递推数组 dp[k][s]:代表 选择 k 个数,因子 5 的总个数为 s 时,因子 2 的总个数
  • 遍历每一个数,当 选择第k个数时,对于 当前数 我们有 选 与 不选两种情况
  • 递推式:dp[k][s] = Math.max(dp[k][s], dp[j-1][s-c5[i]]+c2[i]);

代码展示

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int k = sc.nextInt();
        int[] c5 = new int[n+1];
        int[] c2 = new int[n+1];
        int max = 0;  // 5的最大数量
        for (int i = 1; i <= n; i++) {
            long x = sc.nextLong();
            // 统计每一个数中 因子2,5的个数
            while (x % 5 == 0) {
                c5[i]++;
                x /= 5;
                max++;
            }
            while (x % 2 == 0) {
                c2[i]++;
                x /= 2;
            }
        }
        // 备忘录,自底向上,dp[k][s]:表示 当有 有k个数,有s个因子5时,2的个数
        int[][] dp = new int[k+1][63*n+1];
        System.out.println(dp[0][6]);
        System.out.println(Integer.MIN_VALUE);
        for(int i=0;i<=k;i++)
            Arrays.fill(dp[i], Integer.MIN_VALUE);
        dp[0][0] = 0;

        // 遍历每一个数
        for (int i = 1; i <= n; i++) {
            // 对于每一个数遍历 k次,即当k个数时,包含该数与否的结果
            for (int j = k; j > 0; j--) {
                // 对于j个数,遍历当5的个数为多少时,2的个数
                for (int l = max; l >= c5[i]; l--) {  // 枚举5的个数
                    dp[j][l] = Math.max(dp[j][l], dp[j-1][l-c5[i]]+c2[i]);
                }
            }
        }
        int ans = 0;
        for (int i = 0; i <= max; i++) {
            ans = Math.max(ans, Math.min(dp[k][i], i));  // 找出当有k个数时,对于不同因子5的个数,与因子2的个数的最小值,并与ans求出最大值
        }
        System.out.println(ans);
    }
}

暴力法

  • 统计所有数的 因子2 因子5 的总和。
  • 遍历 n-k 次,之后遍历每一个数,每次选择出一个数:减去一个数,使 sum2,sum5 零个数相对最多(最后留下 k 个数的 sum2,sum5 )
  • 标志数组,vis[i]:代表第 i 个数,是否被访问,每一个数只访问一次

代码展示

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int k = sc.nextInt();
        int sum2 = 0, sum5 = 0;

        long[][] x = new long[n+1][2];  // 输入n个数,x[i][0]: 代表2的个数,x[i][1]:代表因子5的个数
        for (int i = 1; i <= n; i++) {
            long xx = sc.nextLong();
            while (xx % 2 == 0) {
                x[i][0]++;
                xx /= 2;
                sum2++;
            }
            while (xx % 5 == 0) {
                x[i][1]++;
                xx /= 5;
                sum5++;
            }
        }
        boolean[] vis = new boolean[n+1];
        Arrays.fill(vis, false);
        long c = 0;
       	// 遍历 n-k 次,找到 n-k 个数
        for (int i = 1; i <= n-k; i++) {
            int index = 0;
            long max = 0;
            // 遍历所有数
            for (int j = 1; j <= n; j++) {
                // 如果一个数已经被选择,直接跳出
                if (vis[j]) continue;
                c = Math.min(sum2-x[j][0], sum5-x[j][1]);
                if (c > max) {
                    max = c;
                    index = j;
                }
            }
            vis[index] = true;
            sum2 -= x[index][0];
            sum5 -= x[index][1];
        }
        // 输出0的个数
        System.out.println(Math.min(sum2, sum5));
    }
}

实力有限,其他题目后序增加题解
感谢!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chaser&upper

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值