2021 第十二届蓝桥杯 Java B组 (第一场)真题解析

A. ASC(5分)

已知大写字母 A A A A S C I I ASCII ASCII 码为 65 65 65,请问大写字母 L L L A S C I I ASCII ASCII 码是多少?

这题当时看到以为今年题会很水,没想到后面😭


答案:76

代码:
public class Main {

	public static void main(String[] args) {
		// 76
		System.out.println((int)'L');
	}
}

B:卡片(5分)

小蓝有很多数字卡片,每张卡片上都是数字 0 0 0 9 9 9

小蓝准备用这些卡片来拼一些数,他想从 1 开始拼出正整数,每拼一个, 就保存起来,卡片就不能用来拼其它数了。

小蓝想知道自己能从 1 1 1 拼到多少。

例如,当小蓝有 30 30 30 张卡片,其中 0 0 0 9 9 9 3 3 3 张,则小蓝可以拼出 1 1 1 10 10 10, 但是拼 11 11 11 时卡片 1 1 1
已经只有一张了,不够拼出 11 11 11

现在小蓝手里有 0 0 0 9 9 9 的卡片各 2021 2021 2021 张,共 20210 20210 20210 张,请问小蓝可以从 1 1 1 拼到多少?


答案:3181

算法:模拟
  • 开一个cnt数组记录每张卡片用的次数,初始化每张为 2021 2021 2021
  • 循环枚举每个数,直到把某张卡片用尽再break
  • 每次枚举时check每个数的次数,如果某张卡片小于 0 0 0,表示这张卡片已经用尽,返回false,否则返回true

代码:
public class Main{

	static int[] cnt = new int[10];
	public static void main(String[] args) {
		for (int i = 0; i < 10; i ++ ) cnt[i] = 2021;
		
		for (int i = 0; i != -1; i ++ ) {
			if (!check(i))
			{
				System.out.print(i - 1);
				return;
			}
		}
		return;
	}
	
	static boolean check(int x) {
		while (x != 0) {
			int t = x % 10;
			x /= 10;
			if (-- cnt[t] < 0) return false;
		}
		return true;
	}

}

C:直线(10分)

在平面直角坐标系中,两点可以确定一条直线。如果有多点在一条直线上, 那么这些点中任意两点确定的直线是同一条。

给定平面上 2 × 3 2 × 3 2×3 个整点 { ( x , y ) ∣ 0 ≤ x < 2 , 0 ≤ y < 3 , x ∈ Z , y ∈ Z (x, y)|0 ≤ x < 2, 0 ≤ y < 3, x ∈ Z, y ∈ Z (x,y)0x<2,0y<3,xZ,yZ},即横坐标 是 0 0 0 1 1 1 ( 包含 0 0 0 1 1 1 ) 之间的整数、纵坐标是 0 0 0 2 2 2 (包含 0 0 0 2 2 2) 之间的整数 的点。这些点一共确定了 11 11 11 条不同的直线。

给定平面上 20 × 21 20 × 21 20×21 个整点 { ( x , y ) ∣ 0 ≤ x < 20 , 0 ≤ y < 21 , x ∈ Z , y ∈ Z (x, y)|0 ≤ x < 20, 0 ≤ y < 21, x ∈ Z, y ∈ Z (x,y)0x<20,0y<21,xZ,yZ},即横 坐标是 0 0 0 19 19 19 ( 包含 0 0 0 19 19 19 ) 之间的整数、纵坐标是 0 0 0 20 20 20 ( 包含 0 0 0 20 20 20 ) 之 间的整数的点。请问这些点一共确定了多少条不同的直线。


答案: 40257

算法:模拟

我们小学都学过,表示一条直线是 y = k x + b y = kx + b y=kx+b,即判断一条直线是否相同就看他们的斜率( k k k)和截距( b b b)是否相同

  • k = ( y 2 − y 1 ) / ( x 2 − x 1 ) k = (y2 - y1) / (x2 - x1) k=(y2y1)/(x2x1)
  • b = y 1 − k x 1 b = y1 - kx1 b=y1kx1

注意:记得特判当 k k k 不存在时,即这条直线垂直于x轴


代码:
import java.util.List;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

public class Main{
    static Set<String> ans = new HashSet<>();

    public static int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    public static void getKB(int a, int b) {
        int x1 = a / 100, x2 = b / 100;
        int y1 = a % 100, y2 = b % 100;

        // 计算 k 的最简分数
        int up = y1 - y2, down = x1 - x2;
        int div_k = gcd(up, down);
        String K = (up / div_k) + " " + (down / div_k);

        // 特判 k 不存在,即 down = 0 的情况
        if (down == 0) {
            ans.add("x = " + x1);
            return;
        }

        // 代入点 (x1, y1) 来计算 kx 和 y 的分数
        int up_kx = up * x1, up_y = y1 * down;

        // 计算 b = y - kx 的最简分数
        int up_b = up_y - up_kx;
        int div_b = gcd(up_b, down);
        String B = (up_b / div_b) + " " + (down / div_b);

        // 加入答案
        ans.add(K + "  " + B);
    }

    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();

        int x = 19, y = 20;
        for (int i = 0; i <= x; i++) {
            for (int j = 0; j <= y; j++) {
                set.add(i * 100 + j);
            }
        }

        List<Integer> arr = new ArrayList<>(set);
        int len = arr.size();
        for (int i = 0; i < len; i++) {
            int a = arr.get(i);
            for (int j = i + 1; j < len; j++) {
                int b = arr.get(j);
                getKB(a, b);
            }
        }
        System.out.println("ans = " + ans.size());
    }

}


D:货物摆放(10分)

小蓝有一个超大的仓库,可以摆放很多货物。

现在,小蓝有 n n n 箱货物要摆放在仓库,每箱货物都是规则的正方体。小蓝 规定了长、宽、高三个互相垂直的方向,每箱货物的边都必须严格平行于长、 宽、高。 小蓝希望所有的货物最终摆成一个大的立方体。即在长、宽、高的方向上 分别堆 L 、 W 、 H L、W、H LWH 的货物,满足 n = L × W × H n = L × W × H n=L×W×H

给定 n n n,请问有多少种堆放货物的方案满足要求。 例如,当 n = 4 n = 4 n=4 时,有以下 6 6 6 种方案: 1 × 1 × 4 、 1 × 2 × 2 、 1 × 4 × 1 、 2 × 1 × 2 、 2 × 2 × 1 、 4 × 1 × 1 1×1×4、1×2×2、1×4×1、2×1×2、 2 × 2 × 1、4 × 1 × 1 1×1×41×2×21×4×12×1×22×2×14×1×1

请问,当 n = 2021041820210418 n = 2021041820210418 n=2021041820210418 (注意有 16 16 16 位数字)时,总共有多少种方案?


答案: 2430

算法:数学

我们可以从题目给出的例子发现每一个 L 、 W 、 H L、W、H LWH 都是 n n n 的因子

所以这题其实就是求 n n n 的所有因子,然后枚举 L 、 W 、 H L、W、H LWH即可

注意:题目的这个 n n n 是一个16位数字,要用 B i g I n t e g e r BigInteger BigInteger 来接收数字


代码:
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

public class Main{

	public static void main(String[] args) {
		BigInteger n = new BigInteger("2021041820210418");
		List<Long> list = new ArrayList<>();
		for (long i = 1; i * i <= n.longValue(); i ++ ) {
			if (n.longValue() % i == 0) {
				list.add(i);
				if (n.longValue() / i != i) list.add(n.longValue() / i);
			}
		}
		
		int res = 0;
		for (long a : list) 
			for (long b : list)
				for (long c : list)
					if (a * b * c == n.longValue()) res ++;
		System.out.println(res);
	}

}


E:路径(15分)

小蓝学习了最短路径之后特别高兴,他定义了一个特别的图,希望找到图 中的最短路径。

小蓝的图由 2021 2021 2021 个结点组成,依次编号 1 1 1 2021 2021 2021

对于两个不同的结点 a , b a, b a,b,如果 a a a b b b 的差的绝对值大于 21 21 21,则两个结点 之间没有边相连;如果 a a a b b b 的差的绝对值小于等于 21 21 21,则两个点之间有一条 长度为 a a a b b b 的最小公倍数的无向边相连。

例如:结点 1 1 1 和结点 23 23 23 之间没有边相连;结点 3 3 3 和结点 24 24 24 之间有一条无向边,长度为 24 24 24;结点 15 15 15 和结点 25 25 25 之间有一条无向边,长度为 75 75 75

请计算,结点 1 1 1 和结点 2021 2021 2021 之间的最短路径长度是多少。


答案: 10266837

思路:最短路

这题就是一个最短路算法的模板题, 不管是用 F l o y d , D i j k s t r a Floyd,Dijkstra FloydDijkstra 还是 S p f a Spfa Spfa 都可以


代码:

D i j k s t r a Dijkstra Dijkstra

import java.util.Arrays;
import java.util.PriorityQueue;

public class Main{

	static int INF = Integer.MAX_VALUE;
	static int N = 2200, M = N * 50;
	static int n;
	static int[] h = new int[N], e = new int[M], w = new int[M], ne = new int[M];
	static int idx;
	static int[] dist = new int[N];
	static boolean[] st = new boolean[N];
	public static void main(String[] args) {
		n = 2021;
		Arrays.fill(h, -1);
		for (int i = 1; i <= n; i ++ ) {
			for (int j = Math.max(1, i - 21); j <= Math.min(n, i + 21); j ++ ) {
				int d = gcd(i, j);
				add(i, j, i * j / d);
			}
		}
		
		dijkstra();
		
		System.out.println(dist[n]);
	}
	
	private static void dijkstra() {
		PriorityQueue<Integer> heap = new PriorityQueue<>();
		heap.offer(1);
		Arrays.fill(dist, INF);
		dist[1] = 0;
		
		while (!heap.isEmpty()) {
			int t = heap.poll();
			if (st[t]) continue;
			st[t] = true;
			
			for (int i = h[t]; i != -1; i = ne[i]) {
				int j = e[i];
				if (dist[j] > dist[t] + w[i]) {
					dist[j] = dist[t] + w[i];
					heap.offer(j);
				}
			}
		}
	}

	private static void add(int a, int b, int c) {
		e[idx] = b;
		w[idx] = c;
		ne[idx] = h[a];
		h[a] = idx ++;
	}
	
	private static int gcd(int a, int b) {
		return b != 0 ? gcd(b, a % b) : a;
	}

}

S p f a Spfa Spfa

import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;

public class Main{

	static int INF = Integer.MAX_VALUE;
	static int N = 2200, M = N * 50;
	static int n;
	static int[] h = new int[N], e = new int[M], w = new int[M], ne = new int[M];
	static int idx;
	static int[] dist = new int[N];
	static boolean[] st = new boolean[N];
	public static void main(String[] args) {
		n = 2021;
		Arrays.fill(h, -1);
		for (int i = 1; i <= n; i ++ ) {
			for (int j = Math.max(1, i - 21); j <= Math.min(n, i + 21); j ++ ) {
				int d = gcd(i, j);
				add(i, j, i * j / d);
			}
		}
		
		spfa();
		
		System.out.println(dist[n]);
	}
	
	private static void spfa() {
		Deque<Integer> q = new LinkedList<>();
		q.offer(1);
		Arrays.fill(dist, INF);
		dist[1] = 0;
		st[1] = true;
		
		while (!q.isEmpty()) {
			int t = q.poll();
			st[t] = false;
			
			for (int i = h[t]; i != -1; i = ne[i]) {
				int j = e[i];
				if (dist[j] > dist[t] + w[i]) {
					dist[j] = dist[t] + w[i];
					if (!st[j]) {
						q.push(j);
						st[j] = true;
					}
				}
			}
		}
	}

	private static void add(int a, int b, int c) {
		e[idx] = b;
		w[idx] = c;
		ne[idx] = h[a];
		h[a] = idx ++;
	}
	
	private static int gcd(int a, int b) {
		return b != 0 ? gcd(b, a % b) : a;
	}

}

F l o y d Floyd Floyd

public class Main {
	static int[][] graph = new int[2050][2050];
	static final int INF = Integer.MAX_VALUE;
	
	private static void floyd() {
		for (int k = 1; k <= 2021; k++) {
			for (int i = 1; i <= 2021; i++) {
				for (int j = 1; j <= 2021; j++) {
					if (i != j && graph[i][j] > graph[i][k] + graph[k][j]) {
						graph[i][j] = graph[i][k] + graph[k][j];
					}
				}
			}
		}
	}
	
	private static int gcd(int a, int b) {
		return b == 0 ? a : gcd(b, a % b);
	}
	
	public static void main(String[] args) {
		for (int i = 1; i <= 2021; i++) {
			for (int j = 1; j <= 2021; j++) {
				graph[i][j] = INF;
			}
		}
		
		for (int i = 1; i <= 2021; i++) {
			int st = Math.max(i - 21, 1);
			for (int j = st; j <= i; j++) {
				int div = gcd(j, i);
				int lcm = i * j / div;
				graph[i][j] = lcm;
				graph[j][i] = lcm;
			}
		}
		
		floyd();
		
		System.out.println(graph[1][2021]); 
	}
	
}

F:时间显示(15分)

小蓝要和朋友合作开发一个时间显示的网站。在服务器上,朋友已经获取 了当前的时间,用一个整数表示,值为从 1970 1970 1970 1 1 1 1 1 1 00 : 00 : 00 00:00:00 00:00:00 到当前时 刻经过的毫秒数。

现在,小蓝要在客户端显示出这个时间。小蓝不用显示出年月日,只需要显示出时分秒即可,毫秒也不用显示,直接舍去即可。

给定一个用整数表示的时间,请将这个时间对应的时分秒输出。


输入格式
输入一行包含一个整数,表示时间。

输出格式
输出时分秒表示的当前时间,格式形如 H H : M M : S S HH:MM:SS HH:MM:SS,其中 H H HH HH 表示时,值为 0 0 0 23 23 23,MM 表示分,值为 0 0 0 59 59 59 S S SS SS 表示秒,值为 0 0 0 59 59 59。时、分、秒 不足两位时补前导 0 0 0

样例输入 1
46800999

样例输出 1
13:00:00

样例输入 2
1618708103123

样例输出 2
01:08:23

评测用例规模与约定
对于所有评测用例,给定的时间为不超过 1 0 18 10^{18} 1018 的正整数。


算法:模拟

以下用 t i m e time time 表示秒数

  • 先用 t i m e time time 除以 1000 1000 1000 化为秒数
  • 秒数为 t i m e time time % 60 60 60
  • 分钟数为 t i m e time time / 60 % 60
  • 时为 t i m e time time 在上一步的基础上除以 60 60 60 再余 24 24 24

代码:
import java.util.Scanner;

public class Main{

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		long time = sc.nextLong();
		time /= 1000;
		long s = time % 60;
		time /= 60;
		long m = time % 60;
		time /= 60;
		long h = time % 24;
		if (h < 10) System.out.print("0" + h);
		else System.out.print(h);
		
		if (m < 10) System.out.print(":0" + m);
		else System.out.print(":" + m);
		
		if (s < 10) System.out.print(":0" + s);
		else System.out.print(":" + s);
	}

}

G: 最少砝码(20分)在这里插入图片描述


算法:数学

小学数奥题,任意一个数可以表示成三进制的数

  • 结论: p = 3 0 + 3 1 + 3 2 + 3 3 + . . . . . . + 3 m p = 3^0 + 3^1 + 3^2 + 3^ 3 + ...... + 3^m p=30+31+32+33+......+3m

证明:参考博客


代码:
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int num = in.nextInt();
        int sum = 0, pow3 = 1, cnt = 0;

        do {
            sum += pow3;
            pow3 *= 3;
            cnt++;
        } while (sum < num);

        System.out.println(cnt);
    }
}


H:杨辉三角形(20分)

在这里插入图片描述


算法:数学 + 二分

这题是个思维题,想到了很简单,没想到得做很久,当时我直接二维模拟做,估计能骗30分

首先我们都知道,杨辉三角就是组合数,每一个数都可以被表示出来

在这里插入图片描述
如图所示,杨辉三角是左右对称的,所以我们只用看一半即可

在这里插入图片描述

这里我们斜着看每一行,也就是这样看

在这里插入图片描述

我们可以从图中发现除了第一行外,每一行都是单调递增的,又因为杨辉三角就是组合数,所以每一行的最小值可以表示为 C 2 k k C_{2k}^{k} C2kk k k k 是当前行(横着)第几个数如图所示
在这里插入图片描述

因为是斜着的每一行是单调递增的,所以我们可以用二分来找到目标值 n n n, 又因为每一斜行也是单调递增的,所以我们从最里面开始往外找

  • 上边界 l = 2 k l = 2k l=2k
  • 下边界 r = n r = n r=n (如果内层斜行都没找到,一定出现在第2行,因为 C n 1 = n C_{n}^{1} = n Cn1=n

我们知道组合数越靠近中间越大,所以由 n n n 的范围是 1 0 9 10^9 109 可以通过计算器算出 C 34 17 > 1 0 9 , C 32 16 < 1 0 9 C_{34}^{17} > 10^9, C_{32}^{16} < 10^9 C3417>109,C3216<109

所以我们可以枚举每一斜行的开头元素(最小元素) k k k, 从 16 16 16 开始枚举

二分的check就是 如果 C m i d k > = n C_{mid}^{k} >= n Cmidk>=n,取左区间,否则取右区间

当我们找到了这个数的时候即 C r k = = n C_{r}^{k} == n Crk==n

我们通过找规律发现 r r r 为前面共多少行(不算这一行),第一行有 1 1 1 个数,第二行有 2 2 2 个数,第三行有 3 3 3 个数,第 n n n 行有 n n n 个数,也就是前面一共有 r ( 1 + r ) 2 \frac{r(1 +r)}{2} 2r(1+r) 个数, k k k 为这一行第几个(下标都从 0 0 0 开始),所以这个数是第 r ( 1 + r ) 2 + k + 1 \frac{r(1 +r)}{2} + k + 1 2r(1+r)+k+1 个数

注意: 特判第一个数 1 1 1


代码:
import java.util.Scanner;

public class Main {

	static int n;
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		if (n == 1) {
			System.out.println(1);
			return;
		}
		for (int k = 16; ; k -- ) {
			if (check(k)) break;
		}
	}
	
	private static boolean check(int k) {
		long l = k * 2, r = n;
		while (l < r) {
			long mid = l + r >> 1;
			if (C(mid, k) >= n) r = mid;
			else l = mid + 1;
		}
				
		if (C(r, k) != n) return false;
	
		System.out.println(r * (r + 1) / 2 + k + 1);
		
		return true;
	}

	//求组合数
	private static long C(long a, long b) {
		long res = 1;
		for (long i = a, j = 1; j <= b; i --, j ++ ) {
			res = res * i / j;
			if (res > n) return res;
		}
		return res;
	}

}


I. 双向排序(25分)

在这里插入图片描述


算法:栈 + 找规律

这题也是思维题,赛后找规律才找出来,不需要用线段树或者 S p l a y Splay Splay,只用一个栈也可以写
如果暴力用 s o r t sort sort 的话会超时
假设这是我们的原序列
在这里插入图片描述
优化一: 由于一开始的序列是升序的,所以如果一开始的操作是后缀操作的话是没有意义的,序列是不会改变的,所以我们从前缀操作开始看,红色为将要操作的前缀序列
在这里插入图片描述
如果有连续的前缀操作,我们发现只需要进行最长的一个前缀操作即可,因为短的前缀操作后,长的还是要进行操作,为何不直接进行最长的前缀操作呢,后缀操作同理,我们把所有的操作节点存进栈,有两个成员变量,一个是当前操作是前缀操作还是后缀操作,另一个是操作的边界

优化二: 若进行到下图这种情况时
在这里插入图片描述

蓝色为原序列,红色为最长连续前缀,橙色为最长连续后缀

从下图我们发现

  • 原序列 A A A 段严格大于 B B B
  • A A A = = = A 1 A_{1} A1 段, B B B = = = B 1 B_{1} B1
  • 所以 A 1 A_{1} A1 段严格大于 B 1 B_{1} B1
  • A 2 A_{2} A2 = = = A 1 A_{1} A1
  • 所以 A 2 A_{2} A2 段 $ 严格大于 C C C
  • 所以后缀升序操作(橙色)只需要操作 C C C 段即可
    在这里插入图片描述
    对于前缀操作同理 ,只需要操作 C C C 段即可
    在这里插入图片描述
    优化三: 当出现下面这种情况时
    在这里插入图片描述
    也就是在进行一次前缀操作和后缀操作后,下一次的前缀操作在上一次的前缀操作的节点后,这个时候我们可以把前两次操作给删去,直接进行这一次的前缀操作,因为上一次的后缀操作和前缀操作都包含在了这一次的前缀操作内,前两次操作等于是没用的,所以我们只需要保留当前操作即可

另外,我们可以发现在我们一次次操作的过程中,操作的区间是在慢慢变小的,每次操作的时候,序列总有一部分是不需要进行操作的,我们也就可以用一个变量来递减的填入数组中


代码:
import java.util.Scanner;

public class Main {

	static int N = 100010;
	static int n, m;
	static PII[] stk = new PII[N];
	static int[] ans = new int[N];
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		m = sc.nextInt();
		int top = 0;//栈顶
		
		while (m -- > 0) {
			
			int p = sc.nextInt();
			int q = sc.nextInt();
			
			if (p == 0) {
				//求出连续操作的最长前缀
				while (top != 0 && stk[top].x == 0) q = Math.max(q, stk[top -- ].y);
				//优化三
				while (top >= 2 && stk[top - 1].y <= q) top -= 2;
				stk[ ++ top] = new PII(0, q);
			} else if (top != 0) {
				//求出连续操作的最长后缀
				while (top != 0 && stk[top].x == 1) q = Math.min(q, stk[top --].y);
				//优化三
				while (top >= 2 && stk[top - 1].y >= q) top -= 2;
				stk[ ++ top] = new PII(1, q);
			}
		}
		
		//k是递减变量,l为左边界,r为右边界
		int k = n, l = 1, r = n;
		for (int i = 1; i <= top; i ++ ) {
			if (stk[i].x == 0) {
				//若为前缀操作,则(stk[i].y, r]不用操作,直接填数
				while (r > stk[i].y && l <= r) ans[r -- ] = k --;
			} else {
				//若为后缀操作,则[l, stk[i].y)不用操作,直接填数
				while (l < stk[i].y && l <= r) ans[l ++ ] = k --;
			}
			if (l > r) break;
		}
		
		//若l < r, 表示中间还有些数没有填上,操作次数为奇数,则下一次操作为前缀操作
		if (top % 2 == 1) {
			while (l <= r) ans[l ++ ] = k --;
		} else {
			while (l <= r) ans[r -- ] = k --;
		}
		
		for (int i = 1; i <= n; i ++ ) System.out.print(ans[i] + " ");
	}
	
	static class PII {
		int x, y;
		
		public PII(int x, int y) {
			this.x = x;
			this.y = y;
		}
	}

}


J. 括号序列(25分)

在这里插入图片描述

算法: DP

合法的括号序列满足两个性质:

  1. 左右括号数量相同
  2. 任意前缀当中左括号数量不小于右括号数量

我们添加括号一定不会添加一对括号,因为去掉这对括号和加上这对括号,最短序列一定是去掉的,所以左括号和右括号的添加是相互独立的
在这里插入图片描述

考虑到我们只能在空隙中插入括号, 如果我们添加的一对左右括号不是在同一个空隙中, 那么他们显然是互不干扰的;如果是添加在同一个空隙中, 那么他们的添加顺序是唯一的, 只能是)(, 因为如果是()的话, 那我们本次的添加就是无效的, 不满足添加最少的括号使得序列得到匹配。 由此可得, 我们只需要单独计算出添加左括号的方案数, 乘上单独添加右括号的方案数就是答案的数量。

明确了上面那个问题,我们就可以对左右括号进行单独计算了, 我们这里以添加左括号为例。

在这里插入图片描述

我们如果以右括号为端点, 将整个序列进行分割, 那么在分割后的每一小段添加左括号的方案数显然只和这段序列中左括号的数量有关, 因为这段序列里全是左括号, 怎么排列都是一种。所以我们只关注左括号的个数就好了, 更准确的来说, 我们只要关注我们添加的左括号的个数。

状态表示: f [ i ] [ j ] f[i][j] f[i][j]:只考虑 i i i 个括号中, 左括号比右括号多 j j j 个的所有方案的集合
状态转移:

在这里插入图片描述

  • 如果 s [ i ] = = ( s[i] == ( s[i]==( ,那么, f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] f[i][j] = f[i - 1][j - 1] f[i][j]=f[i1][j1]
  • f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − 1 ] + . . . . . . + f [ i − 1 ] [ 0 ] f[i][j] = f[i - 1][j - 1] + f[i - 1][j] + f[i - 1][j - 1] +...... + f[i - 1][0] f[i][j]=f[i1][j1]+f[i1][j]+f[i1][j1]+......+f[i1][0]

这样我们的工作看似就完成了, 但是这个dp的时间复杂度是 n 3 n ^ 3 n3 的, 过不了本题 5 k 5k 5k 的数据, 那么我们就要考虑进行优化

我们可以明显注意到

  • f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − 1 ] + . . . . . . + f [ i − 1 ] [ 0 ] f[i][j] = f[i - 1][j - 1] + f[i - 1][j] + f[i - 1][j - 1] +...... + f[i - 1][0] f[i][j]=f[i1][j1]+f[i1][j]+f[i1][j1]+......+f[i1][0]
  • f [ i ] [ j − 1 ] = f [ i − 1 ] [ j − 1 ] + f [ i − 1 ] [ j − 1 ] + . . . . . . + f [ i − 1 ] [ 0 ] f[i][j - 1] = f[i - 1][j - 1] + f[i - 1][j - 1]+ ...... + f[i - 1][0] f[i][j1]=f[i1][j1]+f[i1][j1]+......+f[i1][0]

那么我们可以得出

  • f [ i ] [ j ] = f [ i − 1 ] [ j + 1 ] + f [ i ] [ j − 1 ] f[i][j] = f[i - 1][j + 1] + f[i][j - 1] f[i][j]=f[i1][j+1]+f[i][j1]

通过上述类似完全背包问题的优化可以将时间复杂度优化到 n 2 n^2 n2

当然对与添加右括号来说, 只需要将序列镜像翻转, 然后当作匹配左括号就可以了


代码:
import java.util.Arrays;
import java.util.Scanner;

public class Main {

	static int N = 5010, MOD = (int)1e9 + 7;
	static int n;
	static char[] str = new char[N];
	static long[][] f = new long[N][N];
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String s = sc.next();
		for (int i = 0; i < s.length(); i ++ )
			str[i + 1] = s.charAt(i);
		n = s.length();
		
		
		long l = work();
		
		reverse();
		
		
		for (int i = 1; i <= n; i ++ ) {
			if (str[i] == '(') str[i] = ')';
			else str[i] = '(';
		}
		
		long r = work();
		
		System.out.println(l * r % MOD);
	}
	
	private static long work() {
		for (int i = 0; i < n; i ++ ) Arrays.fill(f[i], 0);
		f[0][0] = 1;
		for (int i = 1; i <= n; i ++ ) {
			if (str[i] == '(') {
				for (int j = 1; j <= n; j ++ ) {
					f[i][j] = f[i - 1][j - 1];
				}
			} else {
				f[i][0] = (f[i - 1][0] + f[i - 1][1]) % MOD;
				for (int j = 1; j <= n; j ++ ) {
					f[i][j] = (f[i - 1][j + 1] + f[i][j - 1]) % MOD;
				}
			}
		}
		
		for (int i = 0; i <= n; i ++ )
			if (f[n][i] != 0) return f[n][i];
		return -1;
	}

	private static void reverse() {
		for (int i = 1; i < n + 1 / 2; i ++ ) {
			char c = str[i];
			str[i] = str[n - i + 1];
			str[n - i + 1] = c;
		}
	}

}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值