第十二届蓝桥杯国赛Java大学A组题解

目录

A纯质数

题目: 

题解:

B完全日期

题目:

题解:

使用LocalDate枚举日期,判断是否为完全日期

C最小权值

题目:

题解:

分治:

D覆盖

题目:

题解:

dfs:

E123

题目:

题解:

F二进制问题

题目:

题解:

G冰山

题目:

题解(7AC 3TLE):

H和与乘积

题目:

题解:

I异或三角

题目:

题解:

J积木

题目:

题解:

差分数组(详解):

举例:

定义和性质:


A纯质数

题目: 

题解:

 暴力枚举判断即可,最终结果1903

public static void main(String[] args) {
    int count = 0;
    for (int i = 2; i <= 20210605; i++) {
        if (is(i)) count++;
    }
    System.out.println(count);//1903
}

static boolean is(int n) {
    int temp = n;
    while (n > 0) {
        if (!isDigitPrime(n % 10)) return false;
        n /= 10;
    }
    return isPrime(temp);
}

private static boolean isDigitPrime(int t) {
    return t == 2 || t == 3 || t == 5 || t == 7;
}

static boolean isPrime(int n) {
    int s = (int) Math.sqrt(n);
    for (int i = 2; i <= s; i++) {
        if (n % i == 0) return false;
    }
    return true;
}

B完全日期

题目:

 

题解:

使用LocalDate枚举日期,判断是否为完全日期

public static void main(String[] args) {
    LocalDate s = LocalDate.of(2001, 1, 1);
    LocalDate e = LocalDate.of(2021, 12, 31);
    int count = 0;
    while (s.isBefore(e)) {
        int y = s.getYear(), m = s.getMonthValue(), d = s.getDayOfMonth();
        if (is(y, m, d)) count++;
        s = s.plusDays(1);
    }
    System.out.println(count);//977
}

static boolean is(int y, int m, int d) {
    int sum = getDigitSum(y) + getDigitSum(m) + getDigitSum(d);
    int sqrt = (int) Math.sqrt(sum);
    return sqrt * sqrt == sum;
}

private static int getDigitSum(int num) {
    int sum = 0;
    while (num > 0) {
        sum += num % 10;
        num /= 10;
    }
    return sum;
}

C最小权值

题目:

题解:

分治:

令f(n)表示n个结点的二叉树的最小权值,则ans=f(2021)

枚举左子树节点个数left,则右子树节点个数right为n-left-1

则f(n) =  1 + 2*f(left) + 3*(right) + left*left*right 

因为要选择权值最小的一种,所以f(n) = Min( 1 + 2*f(left) + 3*(right) + left*left*right  ) left∈[0,n-1]

对于终止条件,当n==0时,空子树权值为0, 当n==1时,单节点权值为1

public static void main(String[] args) {
    System.out.println(f(2021));//2653631372
}

static long[] memo = new long[2022];//记忆化

/**
 n个节点的最小权值
 */
static long f(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    if (memo[n] != 0) return memo[n];
    long min = 1 + 3 * f(n - 1);//左子树空的情况
    for (int left = 1; left < n; left++) {
        int right = n - left - 1;
        min = Math.min(min, 1 + 2 * f(left) + 3 * f(right) + (long) left * left * right);
    }

    return memo[n] = min;
}

D覆盖

题目:

 

题解:

dfs:

令dfs(a,b)表示(a,b)之前的格子均已覆盖的剩余摆放方案数,则ans=dfs(0,0) 

  1. 如果当前格子(a,b)未被覆盖 
    • 如果右边的格子也未覆盖,那么可以在(a,b),(a,b+1)放置一张横向纸片进行搜索,  dfs(a,b+1) 
    • 如果下边的格子也未覆盖,那么可以在(a,b),(a+1,b)放置一张竖向纸片进行搜索b==7? dfss(a+1,0) : dfs(a,b+1) // 搜索方向一律向右,向右越界则换到下一行开头

    2. 如果当前格子(a,b)已被覆盖,则直接搜索下一个位置, b==7? dfss(a+1,0) : dfs(a,b+1)

 终止条件为a==8时,此时说明全部覆盖了,count++

static boolean[][] bool = new boolean[8][8];
static int count = 0;

public static void main(String[] args) {
    dfs(0, 0);
    System.out.println(count);//12988816
}

public static void dfs(int a, int b) {
    if (a == 8) {
        count++;
        return;
    }
    if (!bool[a][b]) {
        bool[a][b] = true;
        if (b != 7 && !bool[a][b + 1]) {//横向放置
            bool[a][b + 1] = true;
            dfs(a, b + 1);
            bool[a][b + 1] = false;
        }
        if (a != 7 && !bool[a + 1][b]) {//竖向放置
            bool[a + 1][b] = true;
            if (b == 7) dfs(a + 1, 0);
            else dfs(a, b + 1);
            bool[a + 1][b] = false;
        }
        bool[a][b] = false;
    } else {//不能放置
        if (b == 7) dfs(a + 1, 0);
        else dfs(a, b + 1);
    }
}

E123

题目:

 

题解:

令S(n)为这个数列的前n项和, 所以[l,r]项的和可以表示为S(r)-S(l-1)

对于这个数列的前n项和:

令 T(n)为数列{1,2,3,4,5,..}的前n项和

S(n) = (1) + (1+2) +(1+2+3) + ... + (1+2+..+m) + (1+2+..+k) 

       = T(1)+T(2)+...+T(m) + T(k)

T(x)是很好求出的,所以现在只需要得到m和k的确切数值即可 

因为 n = 1 + 2 + 3 + ... + m + k = (1+m)*m / 2 + k

则 (1+m)*m / 2 <= n < (1+m+1)*(m+1) / 2

所以可以用二分查找最大的m,满足(1+m)*m / 2 <=n

而k = n - (1+m)*m / 2

 T(1)+T(2)+...T(m)对于连续的加项可以使用前缀和表示

 

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int T = sc.nextInt();
    for (int i = 0; i < T; i++) {
        long l = sc.nextLong(), r = sc.nextLong(); 
        System.out.println(SRange(l, r)); // 数列第l项至第r项的和
    }
}

static int N = 10000000;
static long[] T = new long[N + 1];// T[n]: 数列{1,2,3,4...}的前n项和
static long[] pre = new long[N + 1];// T的前缀和

static {
    for (int i = 1; i < N; i++) T[i] = T[i - 1] + i;
    for (int i = 1; i < N; i++) pre[i] = pre[i - 1] + T[i];
}

private static long SRange(long l, long r) {
    return S(r) - S(l - 1);
}

/**
 S(n) = (1) + (1+2) +(1+2+3) + ... + (1+2+..+m) + (1+2+..+k)
 = T(1)+T(2)+...+T(m) + T(k)
 */
private static long S(long n) {
    if (n == 0) return 0;
    int m = getM(n);
    int k = (int) (n - (long) m * (m + 1) / 2);
    return pre[m] + T[k];
}

private static int getM(long n) {
    // (m+1)m/2 <= n < (m+2)(m+1)/2
    int left = 1, right = (int) Math.sqrt(2 * n);// m^2 + m <= 2n --> m < sqrt(2n)
    int m = 1;
    while (left <= right) {
        int mid = (left + right) >>> 1;
        if ((long) (mid + 1) * mid / 2 > n) {
            right = mid - 1;
        } else {
            m = mid;
            left = mid + 1;
        }
    }
    return m;
}

F二进制问题

题目:

 

题解:

将n转为二进制, 然后进行数位dp, 枚举出k个1即可

定义f(i,k,isLimit)

  • i表示当前枚举的数位是第几个
  • k表示还需要枚举的1的数量
  • isLimit表示前面枚举的数位都到达n这个上界

那么当i==len(n)时枚举完了全部数位,如果此时k==0则数有效,count++, 否则数无效 

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    long n = sc.nextLong();
    int k = sc.nextInt();
    high = Long.toBinaryString(n);//将n转为二进制
    memo = new long[high.length()][k + 1][2];//记忆化
    for (int i = 0; i < high.length(); i++) {
        for (int j = 0; j <= k; j++) {
            Arrays.fill(memo[i][j], -1);//初始化为-1
        }
    }
    System.out.println(f(0, k, true));// 二进制数位dp
}

static String high;
static long[][][] memo;

/**
 @param i       当前枚举数位
 @param k       剩余可填1的个数
 @param isLimit 前面填的数字是否都到达high的上界
 */
static long f(int i, int k, boolean isLimit) {
    int n = high.length();
    if (i == n) {//已枚举全部数位
        return k == 0 ? 1 : 0;//需要恰好为k个1
    }
    if (memo[i][k][isLimit ? 1 : 0] != -1) return memo[i][k][isLimit ? 1 : 0];//记忆化
    int up;
    if (k == 0) {//不能填1了
        up = 0;
    } else {//还能填1
        up = isLimit ? high.charAt(i) - '0' : 1;//根据上界确定范围,如果前面的数都触达n上界,那么该位也受到限制,否则无限制可1可0
    }
    long ans = 0;
    for (int j = 0; j <= up; j++) {
        ans += f(i + 1, k - (j == 1 ? 1 : 0), isLimit && j == up);
    }
    return memo[i][k][isLimit ? 1 : 0] = ans;
}

G冰山

题目:

 

题解(7AC 3TLE):

使用Map映射冰山体积->冰山对应数量, 由于冰山数量非常大,需要使用BigInteger存储

然后根据每一天的情况进行模拟操作

static BigInteger zero = BigInteger.ZERO, one = BigInteger.ONE;
static BigInteger mod = new BigInteger("998244353");

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);

    int n = scanner.nextInt(); // 初始冰山数量
    int m = scanner.nextInt(); // 观察的天数
    int k = scanner.nextInt(); // 冰山的最大限制

    //  冰山体积 -> 对应的体积的冰山数量
    Map<Integer, BigInteger> VToCount = new HashMap<>();
    for (int i = 0; i < n; i++) {
        int a = scanner.nextInt();
        VToCount.put(a, VToCount.getOrDefault(a, zero).add(one));
    }

    for (int i = 0; i < m; i++) {
        int x = scanner.nextInt(), y = scanner.nextInt(); // 每天冰山的变化量x每天漂来的冰山体积y
        VToCount = changeV(VToCount, x, y, k);//模拟操作
        BigInteger sum = new BigInteger("0");
        // 求每一天的体积之和
        Set<Integer> keys = VToCount.keySet();
        for (int j : keys) {
            sum = sum.add(BigInteger.valueOf(j).multiply(VToCount.get(j)));//v*count
            sum = sum.divideAndRemainder(mod)[1];
        }
        System.out.println(sum);
    }
    scanner.close();
}

public static Map<Integer, BigInteger> changeV(Map<Integer, BigInteger> VToCount, int x, int y, int k) {
    Map<Integer, BigInteger> map = new HashMap<>();//新键一个map,存放操作后的冰山情况
    Set<Integer> Vs = VToCount.keySet();
    for (int v : Vs) {
        BigInteger count = VToCount.get(v);//体积v有count个
        long vNext = v + x;//变化后的体积 
        if (vNext <= 0) continue; //vNext<=0,冰山消失,不存入map
        
        if (vNext <= k) {// 未超出k的限制
            BigInteger countNext = map.getOrDefault((int) vNext, zero).add(count);
            map.put((int) vNext, countNext);
        } else if (vNext > k) {  // 超出k的限制,分裂
            BigInteger countKNext = map.getOrDefault(k, zero).add(count);//保留1块体积k的冰山
            map.put(k, countKNext);
            
            BigInteger bi = BigInteger.valueOf(vNext - k);//每个冰山的其余体积分裂为a-k个体积1的冰山
            BigInteger countOneNext = map.getOrDefault(1, zero).add(bi.multiply(count));
            map.put(1, countOneNext);
        }
    }
    if (y != 0) {
        map.put(y, map.getOrDefault(y, zero).add(one));
    }
    return map;
}

H和与乘积

题目:

 

题解:

static long MAX_VALUE = 400_0000_0000L;
static int maxN = 200_0007;
static long[] a = new long[maxN], pre = new long[maxN];

/**
 积是成倍增长的,积的增长比和要快很多
 唯一的特殊点在于1,乘1不变,和增加
 所以根据单调性,对于一段连续的1区间,最多有1个解
 前缀和pre[i] = Sum( a[0...i] )
 Sum( a[l,r] ) = pre[r] - pre[l-1]
 枚举区间左端点l,维护区间乘积res,再枚举区间右端点r,其中r位置的数不为1
 x 1...1  y1 1..1 y2 ..
 l        r1      r2     r3...
 (rk-1,rk)上最多有一个解,再检查rk是否是一个解,这样就把所有位置的解求出来了
 */
public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int n = sc.nextInt();
    List<Integer> pos = new ArrayList<>();//存储非1数的下标
    pos.add(0);//索引从1开始
    for (int i = 1; i <= n; i++) {
        a[i] = sc.nextInt();
        if (a[i] != 1) pos.add(i);
        pre[i] = pre[i - 1] + a[i];//前缀和
    }
    pos.add(n + 1);//区间终点
    System.out.println(pos);
    long ans = 0;
    for (int l = 1; l <= n; l++) {//枚举区间左端点
        long res = 1;//区间乘积,如果l位置是1,则初始为1,如果l位置不是1,那么now就是l
        int now = 0; //找到下标l之后第一个不为1的数的下标idx,其中idx=pos[now],满足a[idx]!=1 && idx>=l
        for (int i = 0; i < pos.size(); i++) {//TODO 二分
            Integer idx = pos.get(i);
            if (idx >= l) {
                now = i;
                break;
            }
        }

        for (int j = now; j < pos.size(); j++) {//枚举区间右端点(不为1的数)
            int r = pos.get(j);
            int cnt = pos.get(j) - pos.get(j - 1) - 1;//前一个不为1的数到当前不为1的数的区间上1的个数
            long m = pre[r - 1] - pre[l - 1];
            if (res <= m && m - cnt < res) {  //如果连续的1区间内有解
                //m>=res:区间总和大于(等于)乘积; m-cnt<res:区间总和减去1的个数小于乘积 ==> 根据单调关系,必然存在一个位置有解
                ans++;
            }
            res = res * a[r];
            if (r != n + 1 && res == pre[r] - pre[l - 1]) {  //当前不为1的位置 是一个解
                //r!=n+1:n+1是数组越界位置; res == pre[r] - pre[l - 1]:区间乘积等于区间乘积
                ans++;
            }
            if (res >= MAX_VALUE) break;//res太大了,可以提前退出
        }
    }
    System.out.println(ans);
}

I异或三角

题目:

 

题解: 

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int T = sc.nextInt();
    for (int i = 0; i < T; i++) {
        int n = sc.nextInt();
        System.out.println(solve(n) * 3);//三元组位置轮换
    }
}


static long solve(int n) {
    for (int i = 0; i < 32; i++) {
        for (int j = 0; j < 2; j++) {
            Arrays.fill(dp[i][j], -1);
        }
    }
    int cnt = 0;
    while (n > 0) {
        cnt += 1;
        num[cnt] = n & 1;
        n >>= 1;
    }
    return dfs(cnt, true, 0);
}

/**
 设 n >= a >= b >= c >= 1 <br>
 abc每个数位都有2个1,1个0 <br>
 1. a>b, 所以(ai,bi)=(1,0)必须出现在(aj,bj)=(0,1)前面 <br>
 a  ? 0 1 <br>
 b  ? 1 0   -> a的?处必然需要填1,而剩下的1无论分给b还是c都会导致a>b&&a>c不成立 <br>
 c  ? 1 1 <br>
 <br>
 2. a>c, 所以(ai,bi)=(1,1)必须出现在(aj,bj)=(0,1)前面 <br>
 a  ? 0 1 <br>
 b  ? 1 1   -> 同理: a的?处必然需要填1,而剩下的1无论分给b还是c都会导致a>b&&a>c不成立 <br>
 c  ? 1 0 <br>
 <br>
 3. a = b^c < b+c,所以必然存在状态(ai,bi)=(0,1) <br>
 因为b^c是不进位加法,0^0=0+0,0^1=0+1,唯有(bi,ci)=(1,1)时 bi^ci=0 < bi+ci <br>
 <br>
 所以当枚举到a的最后,(0,1),(1,0),(1,1)都存在时,则为有效情况 <br>
 // 因为(0,1)必然出现,又有(0,1)出现时,前面一定有(1,0)和(1,1) <br>

 @param i    当前a所在的二进制位数
 @param limit  前面所选择的数是否全部为上限数
 @param states 是否出现过上述三种状态 1->(0,1),2->(1,0),4->(1,1)
 @return
 */
static long dfs(int i, boolean limit, int states) {

    if (i == 0) return states == 7 ? 1 : 0;
    int idx = limit ? 1 : 0;
    if (dp[i][idx][states] != -1) return dp[i][idx][states];

    int up = limit ? num[i] : 1;
    long res = 0;
    for (int d = 0; d <= up; d++) {
        if (d == 0) { //如果该位置上a为0
            //1. (0, 0, 0)
            res += dfs(i - 1, limit && d == up, states);
            //2. (0, 1)
            if (states >= 6) //选(0, 1) 时,(1, 1),(1, 0)必须要出现
                res += dfs(i - 1, limit && d == up, states | 1);// 110 | 001 = 111
        } else {// d == 1
            //3. (1, 0)
            res += dfs(i - 1, limit && d == up, states | 2); //x0x | 010 = x1x
            //4. (1, 1)
            res += dfs(i - 1, limit && d == up, states | 4); //0xx | 100 = 1xx
        }
    }
    return dp[i][idx][states] = res;
}


static long[][][] dp = new long[32][2][8];
static int[] num = new int[32];

J积木

题目:

 

题解:

令f[i]表示水高为i时被水淹的积木数

对于一个高度h的积木,它对 f [1~h] 都有1点贡献, 对于区间加问题,可以使用差分数组解决

差分数组(详解):

举例:

考虑数组 a=[1,3,3,5,8],对其中的相邻元素两两作差(右边减左边), 得到数组 [2,0,2,3]。

然后在开头补上 a[0],得到差分数组 d=[1,2,0,2,3]

这有什么用呢? 如果从左到右累加 d 中的元素,我们就「还原」回了 a 数组 [1,3,3,5,8]。 这类似求导与积分的概念。

这又有什么用呢? 现在把连续子数组 a[1],a[2],a[3] 都加上 10,得到 a′=[1,13,13,15,8]。

再次两两作差,并在开头补上 a′[0],得到差分数组d′=[1,12,0,2,−7]

对比 d = [1,2,0,2,3] 和 d′ = [1,12,0,2,−7] , 可以发现只有 d[1] 和 d[4] 变化了 , 这意味着对 a 中连续子数组的操作,可以转变成对差分数组 d 中两个数的操作 。

定义和性质:

对于数组 a,定义其差分数组为 d[i]={ a[0],i=0 ; a[i]−a[i−1],i≥1 }

性质 1:从左到右累加 d 中的元素,可以得到数组 a。

性质 2:如下两个操作是等价的。

         操作1: 把 a 的子数组 a[i],a[i+1],⋯,a[j] 都加上 x。

        操作2: 把 d[i] 增加 x,把 d[j+1] 减少 x。

利用性质 2,我们只需要 O(1)) 的时间就可以完成对 a 的子数组的操作。最后利用性质 1 从差分数组复原出数组 a。 !注:也可以这样理解,d[i] 表示把下标 ≥i 的数都加上 d[i]。

static int N = 100000;
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(), m = Int();
    int[] d = new int[N + 1];//差分数组
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            int h = Math.min(Int(), N + 1);
            if (h == 0) continue;
            //f[1~h]+1 <=> d[1]+1,d[h+1]-1
            d[1]++;
            d[h + 1]--;
        }
    }
    int H = Int();
    for (int i = 1; i <= H; i++) {//一次前缀和还原数组f
        d[i] += d[i - 1];
    }
    long sum = 0;
    for (int i = 1; i <= H; i++) {//求前缀和
        sum += d[i];
        System.out.println(sum);
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值