第十三届蓝桥杯省赛Java大学A组题解

本文围绕蓝桥杯Java赛题展开,涵盖裁纸刀、寻找整数、求和等多道题目。针对各题给出题解,涉及记忆化搜索、数论分块、带权并查集等方法,还介绍了贪心算法、二分枚举等技巧,为蓝桥杯相关问题的解决提供了思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

A裁纸刀

题目

题解 

记忆化搜索 

定义f(x,y)为x行y列至少裁的次数

  • 如果选择在第i行上裁, 有转移方程f(x,y) = 1+ f(i,y)+f(x-i,y)  , 第i行裁一次,剩余两边的子问题
  • 如果选择在第j列上裁, 有转移方程f(x,y) = 1+ f(x,j)+f(x,y-j)  , 第j列裁一次,剩余两边的子问题

因为需要次数最少,所以需要对dfs(x,y)取min

当只有一行或者一列时, 只需要裁剩余二维码个数-1即可

最后答案为dfs(20,22)+4, 需要加上整张纸边缘的四刀

public static void main(String[] args) {
    for (int i = 0; i < 25; i++) {
        Arrays.fill(memo[i], -1);
    }
    System.out.println(4 + f(20, 22));// 443, 边缘需要额外的4刀
}

static int[][] memo = new int[25][25];//记忆化数组

/**
 x行y列需要裁多少刀
 */
static int f(int x, int y) {
    //一行一列,一个一个裁
    if (x == 1) return y - 1;
    if (y == 1) return x - 1;
    if (memo[x][y] != -1) return memo[x][y];
    //按行
    int min = Integer.MAX_VALUE;
    for (int i = 1; i < x; i++) {
        min = Math.min(min, 1 + f(i, y) + f(x - i, y));
    }
    //按列
    for (int i = 1; i < y; i++) {
        min = Math.min(min, 1 + f(x, i) + f(x, y - i));
    }
    return memo[x][y] = min;
}

B寻找整数

题目

 

题解 

 首先有一条性质: 如果 a % b = c, 那么对a加上b的任意倍数,a % b的结果不变, 即 a % b = (a+kb) % b

令 x[i] 表示 满足对前i个数取余的结果都能对应余数表的 最小正整数, X[i] 表示 满足对前i个数取余的结果都能对应余数表的 通解

那么有关系式 X[i] = x[i] + k1 * lcm(m1,m2,m3,...,mi)

X[i+1] = x[i+1] + k2 * lcm(m1,m2,...mi+1) 

因为只需要取到某对(k1,k2), 可以使通解相等, 那么两式联立可得:

x[i] + k1 * lcm(m1,m2,m3,...,mi) = x[i+1] + k2 * lcm(m1,m2,...mi+1) 

k1 * lcm(m1,m2,m3,...,mi) 与 k2 * lcm(m1,m2,...mi+1)  的公因子为lcm(m1,m2,m3,...,mi)

所以 x[i+1] = x[i] + t * lcm(m1,..,mi)

其中需要满足 x[i+1] % i == map[i], t为不定参数,枚举t,使x[i+1]满足要求即可

这是一个递推式, 递推项为x和lcm

x[i+1] = x[i] + t * lcm[i] // lcm[i] = lcm(m1,m2,..mi)

lcm[i] = lcm(lcm[i-1], i)

public static void main(String[] args) {
    //map[i]: x mod i的值
    int[] map = new int[]{0, 0, 1, 2, 1, 4, 5, 4, 1, 2, 9, 0, 5, 10, 11, 14, 9, 0, 11, 18, 9, 11, 11, 15, 17, 9, 23, 20, 25, 16, 29, 27, 25, 11, 17, 4, 29, 22, 37, 23, 9, 1, 11, 11, 33, 29, 15, 5, 41, 46};

    long x = 1;//x[i]:满足前i个约束的最小x
    long lcm = 1;//lcm[i]:前i个约束的最小公倍数
    for (int i = 2; i < map.length; i++) {
        while (x % i != map[i]) {
            x += lcm; // x[i] = x[i-1] + t * lcm => x[i] % i == map[i]
        }
        lcm = lcm(lcm, i);
    }
    System.out.println(x);//2022040920220409
}

static long lcm(long a, long b) {
    return a * b / gcd(a, b);
}

static long gcd(long a, long b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}

C求和

题目

 求[1,20230408]的和

题解 

 送分题

public static void main(String[] args) {
    System.out.println((1 + 20230408L) * 20230408 / 2);
}

DGCD

题目

 

题解 

 首先根据gcd(x,y)=gcd(x-y,y)做一个变换 gcd(a+k,b+k)=gcd(a-b,b+k)

 gcd(a-b,b+k)能取到的最大值等于a-b, 当(b+k)是(a-b)的倍数时

所以只需要求出满足 (b+k) % (a-b)== 0 的最小k

设 (b+k) / (a-b) = t

那么有 b+k = t(a-b) >= b

所以求 满足(a-b)* t > b 的最小t, 则 k = (a-b)*t - b

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    long a = sc.nextLong(), b = sc.nextLong();
    if (b > a) {//交换ab,保证a>=b
        long t = a;
        a = b;
        b = t;
    }
    long diff = a - b; // 求 (b+k) % diff == 0 的最小k
    int t = (int) (b / diff + 1);// 求 diff * t > b 的最小t, 则 k=diff*t-b
    System.out.println(diff * t - b);
}

E蜂巢 

题目

题解 

对坐标进行数学变换即可

例如上图的B和C, C坐标可以表示为(2.5 , 1) ,B的坐标可以表示为(-3.5, 3)

与我们熟悉的平面直角坐标系有一点不同就是,按y轴移动时,x轴坐标也会改变

比如说C点要移动到原点O, 它与原点O的y轴差为1, 从C点往下一格, 坐标变为(2,0),再向左两步可到达原点O

也就是说, 两点y轴差的一半可以给x轴距离进行缩减

BC的距离y轴差为2, x轴差为6, 移动2步y, x轴距离减1,x轴差变为5, 最终步数为2+5=7

步数公式为 x轴要走的步数 + y轴要走的步数 = (diffX - diffY/2) + diffY = diffX + diffY/2

如果 diffY/2 > diffX: 那么说明一个点处于另一个点y轴最大x缩减范围上, 保持下降y轴的同时, 此时可以控制方向, 将x轴差控制在0.5的范围内, x轴不需要再另外消耗步数

最终公式是 max(0, diffX - diffY / 2) + diffY

static double[][] direction = new double[][]{
        {-1, 0}, {-0.5, 1}, {0.5, 1}, {1, 0}, {0.5, -1}, {-0.5, -1}
};//移动方向与xy坐标的变换 *斜向移动会使横向距离改变0.5长度
/**
 数学坐标变换
 *斜向移动两步可以缩减一格的横向距离
 ans = 横向移动diffX-diffY/2 + 纵向移动diffY
 */
public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int d1 = sc.nextInt(), p1 = sc.nextInt(), q1 = sc.nextInt();
    int d2 = sc.nextInt(), p2 = sc.nextInt(), q2 = sc.nextInt();
    double[] point1 = change(d1, p1, q1);//转换为(x,y)坐标
    double[] point2 = change(d2, p2, q2);
    double diffX = Math.abs(point1[0] - point2[0]);
    double diffY = Math.abs(point1[1] - point2[1]);
    System.out.println((int) (Math.max(0, diffX - diffY / 2) + diffY)); 
}

static double[] change(int d, int p, int q) {
    double x = 0, y = 0;
    double[] dir = direction[d];
    x += p * dir[0];
    y += p * dir[1];
    dir = direction[(d + 2) % 6];
    x += q * dir[0];
    y += q * dir[1];
    return new double[]{x, y};
}

F全排列的价值 

题目

题解 

对于某个排列 a1 a2 a3 ... ak ... an 和它的反排列 an, an-1,...ak,...a2,a1

现在考虑这两次排列ak的总价值,设ak=v

设 a[1,k-1]中 小于v的有x个数, a[k+1, n] 小于v的有y个数

因为序列由[1,n]组成,那么总共小于ak=v的数就有v-1个

所以x+y=v-1 即 正排列+反排列ak的贡献为v-1

全体元素的贡献为 Sum(ak - 1) ,等于 [0,n-1]的和, n(n-1)/2

也就是说一对正反排列的价值为n(n-1)/2

排列数A(n,n)=n!, 总共有 n!/2对正反排列

所以价值为 n(n-1)/2 * n!/2

因为结果需要对mod取余, 而式子里有除法,除法不适用模性质,这里有三种处理方式(见代码注释)

/**
 sum(n) = n * (n-1) * n! / 4
 法1: 不乘就是除了
 ans = sum(n) % mod
     = n * (n-1) / 2 % mod * mul(3,n) % mod // n * (n-1)有2的因子可以直接除, n!部分不乘2
 法2: 模上的除法提取到模外
 ans = n * (n-1) * n! / 4 % mod
     = n * (n-1) * n! % (4mod) / 4 // 公式 (a / b) % mod = (a % (b*mod)) /b
     = n * (n-1) % (4mod) * n! % (4mod) / 4
 法3: 乘法逆元,乘法代替除法
 ans = n * (n-1) * n! / 4 % mod
     = n * (n-1) * n! * inv(4) % mod // inv(4)表示在mod下的4的乘法逆元, inv(4)=4^(mod-2)
     = n * (n-1) % mod  * n! % mod  * inv(4) % mod
 */
public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    long n = sc.nextInt();
    long ans = n * (n - 1) / 2 % mod;
    for (int i = 3; i <= n; i++) {//2不乘,相当于结果除以2
        ans = (ans * i) % mod;
    }
    System.out.println(ans);
}

G青蛙过河

题目

题解 

假设青蛙的步长为y

如果需要往返x次, 那么对于任意一个长度为y的区间, 区间和必须大于等于2x

如果长度为y的某个区间的和小于2x,那么这个区间就无法支撑x次往返,步长y就不能作为答案

如果y满足了以上条件, 那么一定能够往返x次

因为可以找到一个划分, 使得每个区间的和都大于等于2x,这样每次的按照区间去跳即可

所以可以二分枚举步长,检查在该步长下是否所有区间和都大于等于2x

检查固定长度的子数组和,可以使用前缀和数组快速查询

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

static int Int() {
    try {
        st.nextToken();
    } catch (Exception ignored) {

    }
    return (int) st.nval;
}

public static void main(String[] args) {
    int n = Int(), x = Int();
    //前缀和
    long[] preSum = new long[n];
    for (int i = 1; i < n; i++) {
        int h = Int();
        preSum[i] = h + preSum[i - 1];
    }

    //二分枚举步长
    int l = 0, r = n;
    while (l + 1 != r) {
        int mid = (l + r + 1) >> 1;
        if (check(preSum, mid, x)) {
            r = mid;
        } else {
            l = mid;
        }
    }
    System.out.println(r);
}

/**
 检查长度为y的h子数组和是否都大于等于2x

 @param preSum h数组的前缀和,preSum[i] = sum( h[0,i) )
 @param y      步长
 @param x      往返次数
 */
private static boolean check(long[] preSum, int y, int x) {
    int n = preSum.length;
    long _2x = 2L * x;
    for (int i = 0; i + y < n; i++) {// preSum[i + y] - preSum[i] = h[i,i+y)
        if (preSum[i + y] - preSum[i] < _2x) return false;
    }
    return true;
}

H因数平方和

题目

题解 

g(n) = \sum_{i=1}^nf(i) = a_1*1^2 + a_2*2^2 + ... + a_n*n^2

考虑1出现次数, 每个数都有因子1, 次数a1=n

考虑2出现次数, 每两个数有一个因子2, 次数a2 = floor(n/2)

考虑3出现次数, 每三个数有一个因子3, 次数a3 = floor(n/3)

...

所以g(n) = \sum_{k=1}^n k^2 * \left \lfloor \frac{n}{k} \right \rfloor

对于 \sum{f(x)*g( \left \lfloor \frac{n}{k} \rfloor \right)}这样的式子,可以使用数论分块提高运行效率, 前提是f(x)可以快速地求部分和,或者已经预处理出f(x)的前缀和数组了, 这里为k^2,前n个数的平方和公式为n(n+1)(2n+1)/6

数论分块
i123456789101112
floor(n/i)1264321
leftright

在n=12时,floor(n/i)可以分块为: {1},{2},{3},{4},{5,6},{7,8,9,10,11,12}

对于Sum(floor(n/i))可以分别对每个块进行处理, 如上图n=12,原本需要遍历12个,现在分为了6块,只需要遍历6个即可

对于某个块的左端点left, 如何确定它的块大小, 即块的右端点right的位置

令   \lfloor \frac{n}{left}\rfloor = \lfloor \frac{n}{right}\rfloor= k

那么有  \frac{n}{right} \geqslant k \ \ \ \Rightarrow \ \ \ \frac{n}{k} \geqslant right 

则 right_{max} = \lfloor \frac{n}{k} \rfloor = \lfloor \frac{n}{ \lfloor \frac{n}{left}{} \rfloor} \rfloor

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int n = sc.nextInt();
    long ans = 0;
    for (int l = 1; l <= n; ) {//处理所有块[l,r]
        int x = n / l, r = n / (n / l);
        //x = floor(n/l) ; s(r)-s(l-1) = sum(i^2) l<=i<=r
        ans = ((ans + x * (s(r) - s(l - 1))) % mod + mod) % mod;
        l = r + 1;
    }
    System.out.println(ans);
}

static int mod = 10_0000_0007;
static final int INV = 166666668;//6的乘法逆元

/**
 sum(i^2)  1<=i<=n
 */
static long s(long n) {
    return n * (n + 1) % mod * (2 * n + 1) % mod * INV % mod;
}

I最优清零方案

题目

 

题解 

  1. 区间操作一定优于单点操作
  2. 对于区间[l,r], 假设其中的最小值为min, 则最多可以对[l,r]执行min次操作

假设[0,i]都已变为0, 对于区间[i+1,N), 选择[i+1,i+k]执行 A[j] = min 次操作, 则操作之后 A[j]变为0, [i+1,j-1]长度不足k, 只能单点修改, 剩余[j+1,N)的子问题

对于[0,k]这个区间,设[0,k-1]的最小值A[j]=min, 考虑对[0,k-1]操作和对[1,k]操作

case1: j!=0  

        1. [0,k-1]最多操作min次, 而[1,k]由于加入了A[k], 去掉了A[0] (A[0]>A[j]) , A[k]可能比min更小,[1,k]的可操作次数就小于等于min次

        2. 如果选择操作[1,k], 那么位置0不能区间操作, 只能进行单点修改

综合这两点:[0,k-1]的操作优于[1,k]的操作

case2: j==0   设[0,k-1]的第二小值A[t]=sec

       1. 如果先对[0,k-1]操作,可操作min次, 再对[1,k]操作,可操作sec-min次, 总操作次数为sec

        2.如果先对[1,k]操作,可操作sec次, 0位置不能再操作,单点修改A[0]次, 总操作次数为sec+A[0] 所以对[0,k-1]操作优于对[1,k]操作

贪心: 

从前往后枚举区间, 进行区间操作, 最大操作次数为min, min累加到ans中

每次操作后当前区间不能再进行操作了, 因为当前区间有元素变为0, 此时需要跳转到下一个区间

下一个区间为当前区间的最后一个0位置的下一位

最后剩余的数字都是需要单点修改的,将其累加到ans中

public static void main(String[] args) {
    int N = Int(), K = Int();
    int[] A = new int[N];
    for (int i = 0; i < N; i++) A[i] = Int();
    long ans = 0;
    int left = 0;
    while (left + K <= N) {
        //对[left,right]进行区间操作
        int right = left + K - 1;
        //求[left,right]的最小值
        int min = Integer.MAX_VALUE;
        for (int i = left; i <= right; i++) {
            min = Math.min(min, A[i]);
        }
        //最多操作min次
        if (min != 0) {
            ans += min;
            for (int i = left; i <= right; i++) {
                A[i] -= min;//[left,right]区间都减去min TODO 线段树优化, N<1e6, O(n^2)咋能过的
            }
        }
        //[left,min]都无法再进行区间操作,left需要跳转到min+1位置进行区间操作
        //left跳转到idx[min]+1位置,如果有多个min,跳转到最后的一个
        for (int i = left; i <= right; i++) {
            if (A[i] == 0) left = i;
        }
        left++;
    }
    for (int i : A) ans += i;//剩下的单点修改
    System.out.println(ans);
}

J推导部分和

题目

 

题解 

假如现在已知 sum(a,b)和sum(b+1,c), 那么就可以推导出sum(a,c)

我们定义: sum(a,b)已知 等价于 a-1与b连通

若a与b连通,b与c连通,则 sum(a+1,c) = sum(a+1, b) + sum(b+1,c) , 即a与c连通

带权并查集 

所以可以维护一个并查集, 根据给出的部分和做节点连通, 这样在询问时只需要查看节点是否连通即可判断能否推导部分和

如果能推导,我们还需要输出部分和为多少, 那么我们还需要维护一个权值, 即带权并查集

在并查集中数组fa[i]=j 表示 有i->j的连通关系, 令数组 val[i] 表示 i->j的权值,这个权值就是sum(i+1,j)

并查集的基本操作find: 

find函数查找元所在集合的集合根 

a->b->c 假如 a->b 的权值为x, b->c的权值为y, 即 sum(a+1,b)=x, sum(b+1,c)=y, 那么有sum(a+1,c) = x+y, 所以 a->c 权值为 x+y

所以在find(x)路径压缩时, 如果x的集合根为rx

那么路径压缩fa[x]=rx, 权值压缩val[x]=路径上的边权之和, 因为find函数是递归的, 所以回溯做累加即可, val[x]+=val[oldFa] 原边加上父边

public int find(int x) {
    if (x == fa[x]) return x;//x为根
    int oldFa = fa[x];//存一下父节点
    int root = find(fa[x]);//向父节点走,找集合根
    fa[x] = root;//路径压缩
    val[x] += val[oldFa];//权值压缩, 回溯时oldFa累加了从oldFa到root的值,现在累加到x上
    return root;
}
并查集的基本操作union: 

将节点x和y连通, 即合并x和y所在的两个集合, 合并集合需要按根节点合并, 否则直接让x简单指向y,只是将x从原集合移除并添加到y的集合, 原连通性改变, 正确做法是将集合x的根rx指向集合y的根

假设当前 x所在集合的集合根为rx, y所在集合的集合根为ry, 现在需要连通x和y, x->y权值为v

因为是按根节点合并, rx需要去指向ry, fa[rx]=ry

但rx->ry是未知量,下面推导rx->ry的权值

x到ry有两条路径, x->rx->ry 和 x->y->ry, 但x到ry的权值是唯一的,所以可得等式:

x->rx + rx->ry = x->y + y->ry

所以 rx->ry = x->y + y->ry - x->rx

val[rx] = v + val[y]  -  val[x]

public void union(int x, int y, long v) {//合并节点x和y,x->y权值为v
    int rx = find(x), ry = find(y);//查找所在集合根
    fa[rx] = ry;//合并集合
    val[rx] = val[y] - val[x] + v;//推导rx->ry
}

并查集的基本操作isConnect: 

判断两个节点是否连通, 只需要查看这两个节点的集合根是否相同即可

public boolean isConnect(int x, int y) {
    return find(x) == find(y);
}

本题额外操作query: 

val[i]表示 (i+1)->root的和 sum([i+1,root]) , 现在求[left,right]的和

sum([left,right]) = sum([left,root]) - sum([right+1,root])  = val[left-1] - val[right]

public long query(int left, int right) {
    return val[left - 1] - val[right];
}
解题代码
static StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));

static int Int() {
    try {
        st.nextToken();
    } catch (Exception ignored) {

    }
    return (int) st.nval;
}

static long Long() {
    try {
        st.nextToken();
    } catch (Exception ignored) {

    }
    return (long) st.nval;
}

/**
 若sum(A[x,y])已知,则x-1与y连通
 若a与b连通,b与c连通, 因为 sum(A[a+1,c]) = sum(A[b+1,c]) + sum(A[a+1,b]) ,可得a与c连通
 所以维护一个并查集,若i与j连通,则可推导[i,j]部分和,否则无法推导
 */
public static void main(String[] args) {
    int n = Int(), m = Int(), q = Int();
    Set set = new Set(n);//并查集
    //m个部分和
    for (int i = 0; i < m; i++) {
        int l = Int(), r = Int();
        long s = Long();
        set.union(l - 1, r, s);// sum(a[l,r])=s
    }
    //q次询问
    for (int i = 0; i < q; i++) {
        int l = Int(), r = Int();
        if (!set.isConnect(l - 1, r)) System.out.println("UNKNOWN");
        else System.out.println(set.query(l, r));
    }
}

static class Set {
    int[] fa;//fa[i]=j,表示i与j为同一个集合,j比i高,如果i==j,说明i是集合里最高的(根)
    long[] val; // fa[i]=j && value[i]=x 表示i->j的权值为x

    public Set(int n) {
        fa = new int[n + 1];
        val = new long[n + 1];
        for (int i = 0; i <= n; i++) {
            fa[i] = i;
        }
    }

    public int find(int x) {
        if (x == fa[x]) return x;
        int oldFa = fa[x];
        int root = find(fa[x]);
        fa[x] = root;
        val[x] += val[oldFa];//回溯时oldFa累加了从oldFa到root的值,现在累加到x上
        return root;
    }

    public void union(int x, int y, long v) {
        int rx = find(x), ry = find(y);
        fa[rx] = ry;
        val[rx] = val[y] - val[x] + v;
    }

    public boolean isConnect(int x, int y) {
        return find(x) == find(y);
    }

    /**
     本题应用: val[i]表示 i->root 的和
     要求数组[left,right]的和, 
     sum([left,right]) 
     = sum([left,root]) - sum([right+1,root])  
     = val[left-1] - val[right]
     */
    public long query(int left, int right) {
        return val[left - 1] - val[right];
    }
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值