第13届蓝桥杯javaB组省赛

第13届蓝桥杯javaB组省赛

其他链接

第9届蓝桥杯JavaB组省赛 - Cattle_Horse

第10届蓝桥杯JavaB组省赛 - Cattle_Horse

第11届蓝桥杯JavaB组省赛 - Cattle_Horse

第12届蓝桥杯JavaB组省赛 - Cattle_Horse

试题 A 星期计算

问题描述

已知今天是星期六,请问 \(20^{22}\) 天后是星期几?

注意用数字 \(1\)\(7\) 表示星期一到星期日。

思路一

因为每七天一个循环,所以计算 \(20^{22}\ mod\ 7\) 的值,在从星期六向后数对应的天数即可。

又由于取余运算是封闭的,即乘的过程中取余不影响最终结果,因此可以不使用高精度运算。

public class Main {
    public static void main(String[] args) {
        int diff = 1;
        for (int i = 0; i < 22; ++i) {
            diff *= 20;
            diff %= 7;
        }
        int ans = (6 + diff) % 7;
        if (ans == 0) ans = 7;
        System.out.println(ans);
    }
}

思路二

由同余的性质得,\(20\equiv-1(mod\ 7)\),即 \(20\)\(-1\) 在模 \(7\) 的意义下相等。

所以 \(20^{22}\ mod\ 7={(-1)}^{22}\ mod\ 7=1\)

因此,向后数一天,即星期日。

试题 B 山

问题描述

这天小明正在学数数。

他突然发现有些正整数的形状像一座“山”,比如 \(123565321\)\(145541\),它们左右对称(回文)且数位上的数字先单调不减,后单调不增。

小明数了很久也没有数完,他想让你告诉他在区间 \([2022, 2022222022]\) 中有多少个数的形状像一座“山”。

思路

先判断对应位置的数是否相等,在判断相邻两个数字是否满足单调性质。

转化为字符串寻找对应位置的数更为简便。

public class Main {
    static boolean check(int x) {
        String s = Integer.toString(x);
        for (int left = 0, right = s.length() - 1; left < right; ++left, --right) {
            if (s.charAt(left) != s.charAt(right) || s.charAt(left) > s.charAt(left + 1) || s.charAt(right - 1) < s.charAt(right)) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        int ans = 0;
        for (int i = 2022; i <= 2022222022; ++i)
            if (check(i)) ++ans;
        System.out.println(ans);//3138
    }
}

试题 C 字符统计

问题描述

给定一个只包含大写字母的字符串 \(S\),请你输出其中出现次数最多的字母。

如果有多个字母均出现了最多次,按字母表顺序依次输出所有这些字母。

思路

先记录下每个字母的出现次数,找出出现次数最大的数,再循环判断每个出现次数是否等于最大的。

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int[] numbers = new int[26];
        String line = sc.nextLine();
        for (int i = 0; i < line.length(); ++i) {
            ++numbers[line.charAt(i) - 'A'];
        }
        int max = -1;
        for (int i = 0; i < 26; ++i) {
            max = Math.max(max, numbers[i]);
        }
        for (int i = 0; i < 26; ++i) {
            if (numbers[i] == max) {
                System.out.print((char)(i + 'A'));
            }
        }
        sc.close();
    }
}

试题 D 最少刷题数

问题描述

小蓝老师教的编程课有 \(N\) 名学生,编号依次是 \(1\cdots N\)。第 \(i\) 号学生这学期刷题的数量是 \(A_i\)

对于每一名学生,请你计算他至少还要再刷多少道题,才能使得全班刷题比他多的学生数不超过刷题比他少的学生数。

题目简单来说就是,刷最少的题 让 刷题多的少于或等于刷题少的(用最少的努力达到中等或偏上的排名)

思路一

找到刷题数排在正中间的人,其刷题数记为 \(midValue\)

  1. 如果刷题数大于 \(midValue\),则说明他已经满足刷题多的少于刷题少的了(已经中等偏上了)
  2. 如果刷题数等于 \(midValue\),则需要计算比他刷题数(即 \(midValue\))少的人的个数 和 刷题数比他多的人的个数,确定最少需要超过多少人,由于已经是中位数了,最多只用刷一道题即可达成目标。
  3. 如果刷题数小于 \(midValue\),则需要判断刷题数比 \(midValue\) 少的人的个数 和 多的人的个数,如果多的人的个数大于少的人的个数,则需要刷题数至 \(midValue+1\),否则刷题数至 \(midValue\)

注意:当前这个人多刷题后,刷题数比他少的人数要排去当前这个人。

对于如何知道 \(midValue\) 的值,有三种方法:

  1. 排序后,求中位数,时间复杂度 \(O(nlog(n))\)
  2. 小根堆,与堆排序的方法类似,只需将弹出堆顶元素次数改为 \(k\) 次,时间复杂度 \(O(klog(n))=O(\dfrac{n}{2}log(n))=O(nlog(n))\)
  3. 依据快排相类似的思想,时间复杂度 \(O(\sum{n+\dfrac{n}{4}}+\dfrac{n}{8}+\cdots+1)=O(2n)=O(n)\)

此处使用第三种方法求 \(midValue\)

时间复杂度 \(O(n)\)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.Random;

public class Main {

    public static void swap(int arr[], int l, int r) {
        int temp = arr[l];
        arr[l] = arr[r];
        arr[r] = temp;
    }

    /*
     * 作用:对数组进行划分,选择一个基准pivot,放在它排序后的位置,即左边的数比它小,右边的数比它大
     * 描述:返回基准下标(快速排序所用)
     */
    public static int partition(int[] arr, int l, int r, int pos) {
        int pivot = arr[pos];
        swap(arr, l, pos);
        while (l < r) {
            while (l < r && arr[r] >= pivot) --r;
            arr[l] = arr[r];
            while (l < r && arr[l] <= pivot) ++l;
            arr[r] = arr[l];
        }
        arr[l] = pivot;
        return l;
    }

    public static Random rand = new Random();

    /*
     * 作用:找到数组[l,r]排序后下标为k的数
     */
    public static int nthElement(int[] arr, int l, int r, final int k) {
        int pos = partition(arr, l, r, rand.nextInt(r - l + 1) + l);
        if (pos == k) return arr[pos];
        if (k < pos) return nthElement(arr, l, pos - 1, k);
        return nthElement(arr, pos + 1, r, k);
    }

    static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));

    static int get() throws Exception {
        in.nextToken();
        return (int) in.nval;
    }

    public static void main(String[] args) throws Exception {
        final int n = get();
        int[] a = new int[n], b = new int[n];
        for (int i = 0; i < n; ++i) {
            b[i] = a[i] = get();
        }
        int midValue = nthElement(b, 0, n - 1, n / 2);
        int less = 0, more = 0;
        for (int val : a) {
            if (val < midValue) ++less;
            else if (val > midValue) ++more;
        }
        StringBuilder ans = new StringBuilder();
        for (int val : a) {
            if (val > midValue) {
                ans.append("0 ");
            } else if (val == midValue) {
                if (more <= less)
                    ans.append("0 ");
                else
                    ans.append("1 ");
            } else {
                if (more <= less - 1)
                    ans.append((midValue - val) + " ");
                else
                    ans.append((midValue - val + 1) + " ");
            }
        }
        System.out.println(ans);
    }
}

思路二

前置知识:

  1. 二分 - Cattle_Horse

具体思路见注释。

时间复杂度 \(O(nlog(max(A_i)))\)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.Arrays;

public class Main {
    static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));

    static int get() throws Exception {
        in.nextToken();
        return (int) in.nval;
    }

    public static void main(String[] args) throws Exception {
        int n = get();
        int[] a = new int[n];
        final int N = 100000;
        // num[i]为i的个数
        int[] num = new int[N + 1];
        int max = -1;
        for (int i = 0; i < n; ++i) {
            a[i] = get();
            max = Math.max(max, a[i]);
            ++num[a[i]];
        }
        // sum[i]为比i小的个数
        int[] sum = new int[max + 1];
        for (int i = 1; i <= max; ++i) {
            sum[i] = num[i - 1] + sum[i - 1];
        }
        // 比i小的数的个数为sum[i]
        // 比i大的数的个数为n-sum[i]-num[i]
        StringBuilder ans = new StringBuilder();
        for (int i = 0; i < n; ++i) {
            if (n - sum[a[i]] - num[a[i]] <= sum[a[i]]) {
                ans.append("0 ");
            } else {
                // 定义l最后落到应该刷的最少的题目数
                // 不可能取到max+1
                int l = a[i] + 1, r = max, mid;
                while (l <= r) {
                    mid = l + r >> 1;
                    // 比i刷题少的个数要减去当前这个人
                    if (n - (sum[mid] - 1) - (num[mid] + 1) <= sum[mid] - 1)
                        r = mid - 1;
                    else
                        l = mid + 1;
                }
                ans.append((l - a[i]) + " ");
            }
        }
        System.out.println(ans);
    }
}

试题 E 求阶乘

问题描述

满足 \(N!\) 的末尾恰好有 \(K\)\(0\) 的最小的 \(N\) 是多少?

如果这样的 \(N\) 不存在输出 \(−1\)

对于 \(100\%\) 的数据,\(1 ≤ K ≤ 10^{18}\).

思路

该题与 793. 阶乘函数后 K 个零 - 力扣 几乎相同

前置知识:

  1. \(1\sim n\) 中约数(即因子)含 \(x\) 的数的个数为 \(\left\lfloor\dfrac{n}{x}\right\rfloor\)
  2. \(n!\) 中质因子 \(5\) 的个数不会大于质因子 \(2\) 的个数
  3. 二分 - Cattle_Horse

面试的时候有一道172. 阶乘后的零 - 力扣这样的题,两题思路类似

\(n!\) 末尾 \(0\) 的数量即因子为 \(10\) 的个数,而 \(10=2\times 5\)

因此转化为求 \(n!\) 中质因子 \(2\) 的个数 \(num_2\) 和质因子 \(5\) 的个数 \(num_5\) 的最小值 \(K\) 是对应的最小的 \(n\)

由前置知识 \(2\) 得,\(K=num_5\)

通过枚举 \(N\) ,判断 \(N\) 的阶乘后的零的个数是否等于\(K\) ,筛选出最小的 \(N\)

对于 \(N_1<N_2<N_3\)

  1. \(N_2\) 的阶乘后的零的个数大于 \(K\),则 \(N_3\) 也一定大于 \(K\)
  2. \(N_2\) 的阶乘后的零的个数小于 \(K\),则 \(N_1\) 也一定小于 \(K\)

因此,\(N\) 的取值具有二段性,可以进行二分。

如何求 \(N!\) 分解质因子后的质因子 \(p\) 的个数:

由前置知识 \(1\) 得:\(1\sim n\) 中约数(即因子)含 \(x\) 的数的个数为 \(\left\lfloor\dfrac{n}{x}\right\rfloor\)

\[[1, n]中p的倍数有n_{1}=\left\lfloor\frac{n}{p}\right\rfloor 个,这些数至少贡献出了n_{1}个质因子p。 \]

\[p^{2}的倍数有n_{2}=\left\lfloor\frac{n}{p^{2}}\right\rfloor个,由于这些数已经是 p 的倍数了,为了不重复统计 p 的个数,我们仅考虑额外贡献的 质因子个数 \]

\[即这些数额外贡献了至少n_{2}个质因子p。 \]

\[依此类推, [1, n] 中质因子 p 的个数为\sum_{k=1}\left\lfloor\frac{n}{p^{k}}\right\rfloor \]

\[\sum_{k=1}\Big\lfloor\dfrac{n}{p^k}\Big\rfloor=\sum_{k=1}\Big\lfloor\dfrac{\frac{n}{p^{k-1}}}{p}\Big\rfloor \]

\[可以理解为每次将 p 除去,再计算质因子含 p 的数的个数 \]

static int get1(int n, int p) {
    int ans = 0;
    for (int i = p; i <= n; i *= p) ans += n / i;
    return ans;
}
static int get2(int n, int p) {
    int ans = 0;
    while (n != 0) {
        n /= p;
        ans += n;
    }
    return ans;
}

计算二分上界 的两种方法

  1. 通过计算 \(N=Long_{max}\) 阶乘后 \(0\) 的个数 \(num\),发现\(num=2305843009213693937\approx2\times10^{18}>K_{max}=10^{18}\)
    因此,\(N\) 不会超过 \(Long_{max}\)

  2. \(ans(n)=\sum_{k=1}\Big\lfloor\dfrac{n}{p^k}\Big\rfloor\ge\Big\lfloor\dfrac{n}{p}\Big\rfloor\)

    \(t=\dfrac{n}{p}\),则 \(n=p\times t\),故 \(ans(p\times t)> \lfloor t\rfloor=t\)

    \(ans(5n)>n\)\(N_{max}=5\times K\)

import java.util.Scanner;

public class Main {
    static final int p = 5;

    static long get(long n) {
        long ans = 0;
        while (n != 0) {
            n /= p;
            ans += n;
        }
        return ans;
    }


    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final long k = sc.nextLong();
        // 定义left 最后落在第一个阶乘后0个数大于等于k的数
        long left = 5, right = 5L * k, mid;
        while (left <= right) {
            mid = left + (right - left) / 2;
            if (get(mid) >= k) right = mid - 1;
            else left = mid + 1;
        }
        System.out.println(get(left) == k ? left : -1);
    }
}

试题 F 最大子矩阵

问题描述

小明有一个大小为 \(N \times M\) 的矩阵,可以理解为一个 \(N\)\(M\) 列的二维数组。

我们定义一个矩阵 m 的稳定度 \(f(m)\)\(f(m) = max(m) − min(m)\),其中 \(max(m)\) 表示矩阵 \(m\) 中的最大值,\(min(m)\) 表示矩阵 \(m\) 中的最小值。

现在小明想要从这个矩阵中找到一个稳定度不大于 \(limit\) 的子矩阵,同时他还希望这个子矩阵的面积越大越好(面积可以理解为矩阵中元素个数)。

子矩阵定义如下:从原矩阵中选择一组连续的行和一组连续的列,这些行列交点上的元素组成的矩阵即为一个子矩阵。

思路

原文链接:[执 梗的博客-CSDN博客_第十三届蓝桥杯最大子矩阵](https://blog.csdn.net/m0_57487901/article/details/127094498)

前置知识:

  1. 二分 - Cattle_Horse
  2. 单调队列 - Cattle_Horse
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.ArrayDeque;
import java.util.Deque;

public class Main {
    static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));

    static int get() throws Exception {
        in.nextToken();
        return (int) in.nval;
    }

    static int n, m, limit;
    // max[k][i][j]代表 第k列i行到j行的最大值,min同理
    static int[][][] max, min;

    public static void main(String[] args) throws Exception {
        n = get();
        m = get();
        max = new int[m + 1][n + 1][n + 1];
        min = new int[m + 1][n + 1][n + 1];
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                max[j][i][i] = min[j][i][i] = get();
            }
        }
        limit = get();
        for (int k = 1; k <= m; ++k) {
            for (int i = 1; i <= n; ++i) {
                for (int j = i + 1; j <= n; ++j) {
                    max[k][i][j] = Math.max(max[k][j][j], max[k][i][j - 1]);
                    min[k][i][j] = Math.min(min[k][j][j], min[k][i][j - 1]);
                }
            }
        }
        int ans = -1;
        // 求第i行到第j行满足条件的最大面积矩阵
        for (int i = 1; i <= n; ++i) {
            for (int j = i; j <= n; ++j) {
                // 如果寻找满足条件的左右两条宽的位置,有两个变量
                // 但如果寻找宽为x的满足条件的矩阵,只有一个变量
                // 大矩阵满足条件则其子矩阵一定满足条件,因此可以二分
                // 定义l最后值为第一个不满足条件的最小宽度,则l-1即r,为最大的满足条件的宽度
                // 闭区间
                int l = 1, r = m, mid;
                while (l <= r) {
                    mid = l + r >> 1;
                    if (check(i, j, mid)) l = mid + 1;
                    else r = mid - 1;
                }
                ans = Math.max(ans, (j - i + 1) * r);
            }
        }
        System.out.println(ans);
    }

    // 判断第i行到第j行是否存在长为len的满足条件的矩阵
//    static boolean check(int x1, int x2, int len) {
//        // 以i为左边线位置,len为长的矩阵
//        for (int i = 1; i <= m - len + 1; ++i) {
//            int MAX = max[i][x1][x2], MIN = min[i][x1][x2];
//            for (int j = 1; j < len; ++j) {
//                MAX = Math.max(MAX, max[i + j][x1][x2]);
//                MIN = Math.min(MIN, min[i + j][x1][x2]);
//            }
//            if (MAX - MIN <= limit) return true;
//        }
//        return false;
//    }

    // check可以通过单调队列 (滑动窗口)来优化
    static boolean check(int x1, int x2, int len) {
        //单调递增
        Deque<Integer> MAX = new ArrayDeque<Integer>();
        //单调递减
        Deque<Integer> MIN = new ArrayDeque<Integer>();
        for (int i = 1; i <= m; ++i) {
            // 如果超过窗口长度,去除最前面的
            if (!MAX.isEmpty() && i - MAX.peekFirst() + 1 > len) MAX.pollFirst();
            // 比当前值小的都出队
            while (!MAX.isEmpty() && max[MAX.peekLast()][x1][x2] < max[i][x1][x2]) MAX.pollLast();
            MAX.offerLast(i);
            if (!MIN.isEmpty() && i - MIN.peekFirst() + 1 > len) MIN.pollFirst();
            while (!MIN.isEmpty() && min[MIN.peekLast()][x1][x2] > min[i][x1][x2]) MIN.pollLast();
            MIN.offerLast(i);
            //说明此时窗口长度已经足够
            if (i >= len && max[MAX.peekFirst()][x1][x2] - min[MIN.peekFirst()][x1][x2] <= limit) return true;
        }
        return false;
    }
}

试题 G 数组切分

问题描述

已知一个长度为 \(N\) 的数组:\(A_1,A_2,A_3,\cdots A_N\) 恰好是 \(1\sim N\) 的一个排列。现

在要求你将 \(A\) 数组切分成若干个 (最少一个,最多 \(N\) 个) 连续的子数组,并且

每个子数组中包含的整数恰好可以组成一段连续的自然数。

例如对于 \(A = \{1, 3, 2, 4\}\), 一共有 \(5\) 种切分方法:

\(\{1\}\{3\}\{2\}\{4\}\):每个单独的数显然是 (长度为 \(1\) 的) 一段连续的自然数。

\(\{1\}\{3, 2\}\{4\}\)\(\{3, 2\}\) 包含 \(2\)\(3\),是 一段连续的自然数,另外 \(\{1\}\)\(\{4\}\) 显然

也是。

\(\{1\}\{3, 2, 4\}\)\(\{3, 2, 4\}\) 包含 \(2\)\(4\),是 一段连续的自然数,另外 \(\{1\}\) 显然也是。

\(\{1, 3, 2\}\{4\}\)\(\{1, 3, 2\}\) 包含 \(1\)\(3\),是 一段连续的自然数,另外 \(\{4\}\) 显然也是。

\(\{1, 3, 2, 4\}\):只有一个子数组,包含 \(1\)\(4\),是 一段连续的自然数。

思路一(40%)

前置知识:

  1. 剪枝 - Cattle_Horse

每次检测当前所选区间是否为一段连续数字

  1. 如果是连续的数字,则向后继续分割
  2. 如果不是连续的数字,则扩大当前区间,直到数组结尾

对于判断区间 \([l,r]\) 内的数字是否为连续的数字,有一个小技巧

因为是连续的,若是连续的数字,则 \(max-min=r-l\)

import java.util.Scanner;

public class Main {
    static int n;
    static int[] a;
    static int[] temp;
    static int ans = 0;

    //检测[l,r]区间范围内的某一排列是否为连续的数字
    static boolean check(int l, int r) {
        int MAX = -1;
        int MIN = n;
        for (int i = l; i <= r; ++i) {
            MAX = Math.max(MAX, a[i]);
            MIN = Math.min(MIN, a[i]);
        }
        return MAX - MIN == r - l;
    }

    static void solve(int now) {
        if (now == n) {
            ++ans;
            if (ans >= 1000000007) ans -= 1000000007;
        }
        //以[now,i]为一块拆分
        for (int i = now; i < n; ++i) {
            if (check(now, i)) {
                solve(i + 1);
            }
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        a = new int[n];
        temp = new int[n];
        for (int i = 0; i < n; ++i) a[i] = sc.nextInt();
        solve(0);
        System.out.println(ans);
    }
}

发现同一段区间进行了多次 \(check\) 判断,采用记忆化数组进行优化。

但由于数据范围过大,仍仅通过 \(40\%\) 测试样例。

import java.util.Scanner;

public class Main {
    static int n;
    static int[] a;
    //记忆化数组
    //book[i][j]=0:代表区间[i,j]未判断
    //book[i][j]=1:代表区间[i,j]已判断是连续数字
    //book[i][j]=-1:代表区间[i,j]已判断不是连续数字
    static int[][] book;
    static int ans = 0;

    //检测[l,r]区间范围内的某一排列是否为连续的数字
    static boolean check(int l, int r) {
        //如果未判断
        if (book[l][r] == 0) {
            int MAX = -1;
            int MIN = n;
            for (int i = l; i <= r; ++i) {
                MAX = Math.max(MAX, a[i]);
                MIN = Math.min(MIN, a[i]);
            }
            if (MAX - MIN == r - l) {
                book[l][r] = 1;
                return true;
            }
            book[l][r] = -1;
            return false;
        }
        return book[l][r] == 1;
    }

    static void solve(int now) {
        if (now == n) {
            ++ans;
        }
        //以[now,i]为一块拆分
        for (int i = now; i < n; ++i) {
            if (check(now, i)) {
                solve(i + 1);
            }
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        a = new int[n];
        book = new int[n][n];
        for (int i = 0; i < n; ++i) a[i] = sc.nextInt();
        solve(0);
        System.out.println(ans);
    }
}

思路二

思路来源:执 梗的博客-CSDN博客_蓝桥杯数组切分

思路一中提到,若是区间 \([l,r]\) 连续的数字,则 \(max-min=r-l\)

对于序列 \([1,n]\)\(1\leq k\leq n\),若已知 \([1,1],[1,2],\cdots,[1,k-1]\) 的划分数量分别为 \(x_1,x_2,\cdots,x_{k-1}\),则 \([1,k]\) 的划分数为 \([1,k]是否为一串连续数字+\sum_{i=1}^{k-1}x_i\times[[i+1,k]是否为一串连续数字]\)

如序列 \(1,3,2\),如果知道:

  • \(\{1\}\) 的划分数为 \(1\)
  • \(\{1,3\}\) 的划分数为 \(1\)

\(\{1,3,2\}\) 的划分数为 \(1+1\times 1+1\times 1 =3\)

\(\{1,3,2\}\) 是一串连续数字,\(1\)

\(\{3,2\}\) 是一串连续数字,\(x_1\times1=1\times 1\)

\(\{2\}\) 是串连续数字,\(x_2\times 1=1\times 1\)

如序列 \(1,3,2,4\),知道上述三个划分数,则 \(\{1,3,2,4\}\) 的划分数为 \(1+1\times1+1\times 0+3\times 1=5\)

\(\{1,3,2,4\}\) 是一串连续数字,\(1\)

\(\{3,2,4\}\) 是一串连续数字,\(x_1\times 1=1\times 1\)

\(\{2,4\}\) 不是一串连续数字,\(x_2\times 0=0\)

\(\{4\}\)是一串连续数字,\(x_3\times 1=3\times 1\)

同时,求区间是否为一串连续数字需要用到该区间的最大值及最小值,于是可以选择倒着进行上述过程,利用求的上一个区间的最大值和最小值

如上述序列 \(1,3,2,4\)

区间 \(\{4\}\)\(max=4\)\(min=4\)

区间 \(\{2,4\}\)\(max=Math.max(max,2),min=Math.min(min,2)\)

区间 \(\{3,2,4\}\)\(max=Math.max(max,3),min=Math.min(min,3)\)

区间 \(\{1,3,2,4\}\)\(max=Math.max(max,1),min=Math.min(min,1)\)

综上所述,

\(dp[k]\)\([1,k]\) 的划分数,则 \(dp[k]=[1,k]是否为一串连续数字+\sum_{i=1}^{k-1}dp[i]\times[[i+1,k]是否为一串连续数字]\)

import java.util.Scanner;

public class Main {
    static final int MOD = 1000000007;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int n = sc.nextInt();
        int[] a = new int[n + 1], dp = new int[n + 1];
        for (int i = 1; i <= n; ++i) a[i] = sc.nextInt();
        for (int r = 1; r <= n; ++r) {
            int MAX = Integer.MIN_VALUE, MIN = Integer.MAX_VALUE;
            for (int l = r; l >= 2; --l) {
                MAX = Math.max(MAX, a[l]);
                MIN = Math.min(MIN, a[l]);
                //如果[l,r]是一串连续数字
                if (MAX - MIN == r - l) dp[r] = (dp[r] + dp[l - 1]) % MOD;
            }
            MAX = Math.max(MAX, a[1]);
            MIN = Math.min(MIN, a[1]);
            if (MAX - MIN == r - 1) dp[r] = (dp[r] + 1) % MOD;
        }
        System.out.println(dp[n]);
    }
}

试题 H 回忆迷宫

问题描述

爱丽丝刚从一处地下迷宫中探险归来,你能根据她对于自己行动路径的回忆,帮她画出迷宫地图吗?

迷宫地图是基于二维网格的。爱丽丝会告诉你一系列她在迷宫中的移动步骤,每个移动步骤可能是上下左右四个方向中的一种,表示爱丽丝往这个方向走了一格。你需要根据这些移动步骤给出一个迷宫地图,并满足以下条件:

  1. 爱丽丝能在迷宫内的某个空地开始,顺利的走完她回忆的所有移动步骤。

  2. 迷宫内不存在爱丽丝没有走过的空地。

  3. 迷宫是封闭的,即可通过墙分隔迷宫内与迷宫外。任意方向的无穷远处视为迷宫外,所有不与迷宫外联通的空地都视为是迷宫内。(迷宫地图为四联通,即只有上下左右视为联通)

  4. 在满足前面三点的前提下,迷宫的墙的数量要尽可能少。

【输入格式】

第一行一个正整数 \(N\),表示爱丽丝回忆的步骤数量。

接下来一行 \(N\) 个英文字符,仅包含 \(UDLR\) 四种字符,分别表示上(\(Up\))、下(\(Down\))、左(\(Left\))、右(\(Right\))。

【输出格式】

请通过字符画的形式输出迷宫地图。迷宫地图可能包含许多行,用字符 ‘*’表示墙,用 ‘ ’(空格)表示非墙。

你的输出需要保证以下条件:

  1. 至少有一行第一个字符为 ‘*’。

  2. 第一行至少有一个字符为 ‘*’。

  3. 每一行的最后一个字符为 ‘*’。

  4. 最后一行至少有一个字符为 ‘*’。

【样例输入】
17
UUUULLLLDDDDRRRRU

【样例输出】
 *****
*     *
* *** *
* *** *
* *** *
*     *
 *****

【样例说明】

爱丽丝可以把第六行第六个字符作为起点。

外墙墙墙墙墙外

墙内内内内内墙

墙内墙墙墙内墙

墙内墙墙墙内墙

墙内墙墙墙内墙

墙内内内内内墙

外墙墙墙墙墙外

【评测用例规模与约定】

对于所有数据,\(0 < N ≤ 100\).

思路一(60%)

见代码注释

import java.util.Scanner;

public class Main {

    static int n;
    static char[] s;
    static int[][] map;

    //UDLR(注意此处对应的是计算机中二维矩阵中每个点的变化,与坐标系不同)
    static final int[] dx = {-1, 1, 0, 0}, dy = {0, 0, -1, 1};

    static int direction(char c) {
        if (c == 'U') return 0;
        if (c == 'D') return 1;
        if (c == 'L') return 2;
        return 3;
    }

    static int minX = 0, maxX = 0, minY = 0, maxY = 0;

    //确定边界及起点
    static void go1() {
        int nowX = 0, nowY = 0;
        for (char c : s) {
            int i = direction(c);
            nowX += dx[i];
            nowY += dy[i];
            minX = Math.min(minX, nowX);
            maxX = Math.max(maxX, nowX);
            minY = Math.min(minY, nowY);
            maxY = Math.max(maxY, nowY);
        }
    }

    // 标记走过的路 1为走过
    static void go2() {
        int nowX = -minX;
        int nowY = -minY;
        //要先标记起点!!
        map[nowX][nowY] = 1;
        for (char c : s) {
            int i = direction(c);
            nowX += dx[i];
            nowY += dy[i];
            map[nowX][nowY] = 1;
        }
    }

    //标记当前点四周(除了要走的路)为墙
    static void go3() {
        int nowX = -minX;
        int nowY = -minY;
        //要先标记起点!!
        for (int i = 0; i < 4; ++i)
            if (map[nowX + dx[i]][nowY + dy[i]] == 0)
                map[nowX + dx[i]][nowY + dy[i]] = -1;
        for (char c : s) {
            int i = direction(c);
            nowX += dx[i];
            nowY += dy[i];
            for (i = 0; i < 4; ++i)
                if (map[nowX + dx[i]][nowY + dy[i]] == 0)
                    map[nowX + dx[i]][nowY + dy[i]] = -1;
        }
    }

    public static void main(String[] args) {
        //输入
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        sc.nextLine();
        s = sc.nextLine().toCharArray();
        //确定边界及起点,并多出两个给边界留位置
        go1();
        --minX;
        --minY;
        ++maxX;
        ++maxY;
        map = new int[maxX - minX + 1][maxY - minY + 1];
        //标记走过的路
        go2();
        //标记四周障碍物,只给未走过的路标记障碍物
        go3();
        //输出
        for (int i = 0; i < map.length; ++i) {
            //找到每行最后一个墙的位置
            int end = 0, j, start = Integer.MAX_VALUE;
            for (j = 0; j < map[i].length; ++j)
                if (map[i][j] == -1) {
                    end = Math.max(end, j);
                    start = Math.min(start, j);
                }
            //在第一个墙之前都为墙外,输出空格,在第一个墙和最后一个墙内没走过的路都是墙
            for (j = 0; j < start; ++j) System.out.print(' ');
            for (j = start; j <= end; ++j) {
                if (map[i][j] == 1) System.out.print(' ');
                else System.out.print('*');
            }
            System.out.println();
        }
    }
}

错误点:

  1. 上述代码最后判断墙内墙外时,仅仅判断了 \(x\) 轴,还应判断 \(y\)
  2. 之前每次都没有对起始点做出相应决策,如初始化 \(minX,minY,maxX,maxY\) 均应初始化为 \(0\),代码中 \(go1,go2\) 一开始均未标记起始点相关信息
  3. 当预留位置后,起始点位置会发生变化,之前未注意
  4. 计算机二维矩阵中的 \(UDLR\) 与坐标轴不同,如计算机二维矩阵中,\(U\) 向上走一步 对应 \(map[x-1][y]\) 一维位置 \(-1\)

思路二(100%)

由于需要判断墙内墙外,可以先走路标记四周,再 \(dfs\) 标记墙外部分(条件是墙内外不连通)

import java.util.Scanner;

public class Main {

    static int n;
    static char[] s;
    static int[][] map = new int[203][203];
    static final int startX = 101, startY = 101;

    //UDLR(注意此处对应的是计算机中二维矩阵中每个点的变化,与坐标系不同)
    static final int[] dx = {-1, 1, 0, 0}, dy = {0, 0, -1, 1};

    static int direction(char c) {
        if (c == 'U') return 0;
        if (c == 'D') return 1;
        if (c == 'L') return 2;
        return 3;
    }

    static int minX = startX, maxX = startX, minY = startY, maxY = startY;

    //确定墙边界并标记走过的路及其四周为墙(除走过的路外)
    //1为走过的路,2为墙
    static void go() {
        int nowX = startX, nowY = startY;
        //注意处理起点
        map[nowX][nowY] = 1;
        for (int i = 0; i < 4; ++i)
            map[nowX + dx[i]][nowY + dy[i]] = 2;
        for (char c : s) {
            int i = direction(c);
            nowX += dx[i];
            nowY += dy[i];
            //标记走过的路
            map[nowX][nowY] = 1;
            for (i = 0; i < 4; ++i)
                if (map[nowX + dx[i]][nowY + dy[i]] == 0)
                    map[nowX + dx[i]][nowY + dy[i]] = 2;
            minX = Math.min(minX, nowX);
            maxX = Math.max(maxX, nowX);
            minY = Math.min(minY, nowY);
            maxY = Math.max(maxY, nowY);
        }
        //最后要算上墙的边界
        --minX;
        --minY;
        ++maxX;
        ++maxY;
    }

    //标记墙外部分,且只走未走过的路
    static void dfs(int x, int y) {
        map[x][y] = 3;
        for (int i = 0; i < 4; ++i) {
            int nx = x + dx[i], ny = y + dy[i];
            //剪枝优化出框了,则不走
            if (nx < minX - 1 || nx > maxX + 1 || ny < minY - 1 || ny > maxY + 1 || map[nx][ny] != 0) continue;
            dfs(nx, ny);
        }
    }

    public static void main(String[] args) {
        //输入
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        sc.nextLine();
        s = sc.nextLine().toCharArray();
        go();
        //(minX-1,minY-1)一定在墙外
        dfs(minX - 1, minY - 1);
        for (int x = minX; x <= maxX; ++x) {
            for (int y = minY; y <= maxY; ++y) {
                //走的路或者墙外均为空格
                if (map[x][y] == 3 || map[x][y] == 1) System.out.print(' ');
                else System.out.print('*');
            }
            System.out.println();
        }
    }
}

收获

  1. 上述起始点其实可以不用改变,当 \(map\) 足够大时,起始点作为中间点,左右均不会越界。如上题,最多走 \(100\) 步,将 \(map\) 设置大小为 \([203][203]\),起始点设置为 \((101,101)\) 一定不会越界
  2. 一定要注意初始化问题,起始点相关信息
  3. 计算机二维矩阵中的 \(UDLR\) 与坐标轴不同,如计算机二维矩阵中,\(U\) 向上走一步 对应 \(map[x-1][y]\) 一维位置 \(-1\)
  4. 自己构造样例时,不应选择过于特殊的样例。如上题,不应构造结果为中心对称的样例,这会导致不能准确判断上下移动是否正确

参考资料

执 梗的博客-CSDN博客_第十三届蓝桥杯最大子矩阵

执 梗的博客-CSDN博客_蓝桥杯数组切分

第十三届蓝桥杯JavaB组部分题解 - GhostLX - 知乎

posted @ 2023-01-06 16:39  Cattle_Horse  阅读(389)  评论(0编辑  收藏  举报