写在开头: 读者们,俯听我一言:本篇文章全面的讲解了各个知识点,并且为每一个知识点提供了模板,只要掌握其中的要点,您轻松就能够获得省一。如果您在阅读过程中有任何疑问或者纰漏之处,请随时私信联系我,我的耐心答复必将令您满意🤩。
一、数论
1.判断质数
注意 i i i 的终止条件本来是 i < = s q r t ( n u m ) i<=sqrt(num) i<=sqrt(num),但是计算开方太慢了,所以用 i ∗ i < = n u m i * i<=num i∗i<=num,
但是 i ∗ i i*i i∗i容易爆int,所以最终改成 i < = n u m / i i <=num / i i<=num/i,后面的其他代码也会采用这种写法,希望不会搞晕。
public static boolean is_prime(int num) {
for (int i = 2; i <= num / i; i++) {
if (num % i == 0) {
return false;
}
}
return true;
}
2.最大公约数
public static int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
3.最小公倍数
a和b最小公倍数是 a ∗ b g c d ( a , b ) \frac{a*b}{gcd(a,b)} gcd(a,b)a∗b,由于a*b可能爆int,所以采用先除再乘, l c m ( a , b ) = a g c d ( a , b ) ∗ b lcm(a,b)=\frac{a}{gcd(a,b)}*b lcm(a,b)=gcd(a,b)a∗b
public static int lcm(int a, int b){
return a/gcd(a,b)*b;
}
public static int gcd(int a, int b) {
while (b != 0) {
int r = a % b;
a = b;
b = r;
}
return a;
}
4.筛质数
下面的两种方法可以得到 小于等于n 的所有质数。
要想一次性得到很多质数,那么就要使用质数筛法,质数筛法有很多方法,现在给出一种 埃及筛法。这种方法写起来比较简单,而且比较快,强烈推荐这种写法。时间复杂度是: O ( n l o g l o g n ) O(nloglogn) O(nloglogn)
public static void getPrimes(int n) {
boolean[] is_prime = new boolean[n + 1];
Arrays.fill(is_prime, true);
for (int i = 2; i <= n / i; i++) {
if (is_prime[i]) {
for (int j = i * i; j <= n; j += i) {
is_prime[j] = false;
}
}
}
for (int i = 2; i <= n ; i++) {
if (is_prime[i]){
System.out.println(i);
}
}
}
5.欧拉函数
欧拉函数:用于求小于等于n的数与n互质的个数是几个。
欧拉函数是积性函数,也就是 f ( a ∗ b ) = f ( a ) ∗ f ( b ) f(a*b)=f(a)*f(b) f(a∗b)=f(a)∗f(b)
public static int f(int n) {
int res = n;
for (int i = 2; i <= n / i; i++) {
if (n % i == 0) {
res -= res / i;
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
res -= res / n;
}
return res;
}
6.区间欧拉函数
该代码用于求小于等于n的数,它们每一个欧拉函数值φ(n)。
此外,当n为质数时, φ ( n ) = n − 1 φ(n) = n-1 φ(n)=n−1
public static int[] oula(int n) {
int[] res = new int[n + 1];
for (int i = 0; i < res.length; i++) {
res[i] = i;
}
for (int i = 2; i <= n; i++) {
if (res[i] == i) {
for (int j = i; j <= n; j += i) {
res[j] -= res[j] / i;
}
}
}
return Arrays.copyOfRange(res, 1, n + 1);
}
7.计算所有约数个数
12的约数有:1,2,3,4,6,12
所以12的约数个数是6
public static int count(int n) {
public static long getSum(int n) {
long result = 0L;
for (int i = 1; i <= n / i; i++) {
if (n % i == 0) {
result++;
if (i * i != n) {
result ++;
}
}
}
return result;
}
}
8.计算所有约数和
12的约数有:1,2,3,4,6,12
所以12的约数个数是28
public static long getSum(int n) {
long result = 0L;
for (int i = 1; i <= n / i; i++) {
if (n % i == 0) {
result += i;
if (i * i != n) {
result += n / i;
}
}
}
return result;
}
9.计算质因子个数
12的约数有:1,2,3,4,6,12
所以12的质约数个数是2
public static void count(int n) {
int count = 0;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
count++;
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
count++;
}
System.out.println(count);
}
10.扩展欧几里得
扩展欧几里得:用途是求解 a x + b y = g c d ( a , b ) ax+by = gcd(a,b) ax+by=gcd(a,b) 中的 x 和 y x和y x和y 的值
已知,gcd(a,b) = d, 则存在 x,y 使得 a x + b y = d = g c d ( a , b ) ax+by = d = gcd(a,b) ax+by=d=gcd(a,b)
特别的,如果a,b互质,当且仅当 a x + b y = 1 ax+by=1 ax+by=1
如果 v % d = 0 v\%d=0 v%d=0,那么 a x + b y = v ax+by=v ax+by=v 的解是 a x + b y = d ax+by=d ax+by=d 的解的 v d \frac{v}{d} dv 倍数
public static int[] ex_gcd(int a, int b) {
if (b == 0) {
return new int[]{a, 1, 0};
} else {
int[] result = ex_gcd(b, a % b);
return new int[]{result[0], result[2], result[1] - (a / b) * result[2]};
}
}
12.快速幂
Java中有Biginteger类,可以实现该操作。
public static int quickPower(int a, int b) {
if (b == 1) return a;
int half = quickPower(a, b / 2);
if (b % 2 == 0) {
return half * half;
} else {
return half * half * a;
}
}
13.快速幂取模
Java可以用Biginteger类的power和mod方法,更方便。但是,这种思想也要掌握。
public static int quickPowerMod(int a, int b, int c) {
if (b == 1) return a;
int half = quickPowerMod(a, b / 2, c) % c;
if (b % 2 == 0) {
return half * half;
} else {
return half * half * a;
}
}
14.欧拉定理
用 φ ( n ) φ(n) φ(n) 表示n的欧拉函数值,那么:
欧拉定理:若a和n互质,则 a φ ( n ) = 1 ( m o d n ) a^{φ(n)}=1(mod\ n) aφ(n)=1(mod n)
费马定理:若a和n互质,并且n为质数,那么 φ(n) = n-1,此时 a n − 1 = 1 ( m o d n ) a^{n-1}=1(mod\ n) an−1=1(mod n)
根据上面的两个定理,我们可以得到 a关于n的逆元:
若a和n互质,则 a − 1 ( m o d n ) = a φ ( n ) − 1 a^{-1}(mod\ n)=a^{φ(n) -1} a−1(mod n)=aφ(n)−1
若a和n互质,并且n为质数,那么 φ(n) = n-1,此时 a − 1 ( m o d n ) = a n − 2 a^{-1}(mod\ n)=a^{n-2} a−1(mod n)=an−2
15.求a关于b的逆元
若a和n互质,则 a − 1 ( m o d n ) = a φ ( n ) − 1 a^{-1}(mod\ n)=a^{φ(n) -1} a−1(mod n)=aφ(n)−1
若a和n互质,并且n为质数,那么 a − 1 ( m o d n ) = a n − 2 a^{-1}(mod\ n)=a^{n-2} a−1(mod n)=an−2
使用快速幂
a − 1 = q u i c k P o w e r M o d ( a , n − 2 , n ) a^{-1}=quickPowerMod(a,n-2,n) a−1=quickPowerMod(a,n−2,n)
public static int quickPowerMod(int a, int b, int c) {
if (b == 1) return a;
int half = quickPowerMod(a, b / 2, c) % c;
if (b % 2 == 0) {
return half * half;
} else {
return half * half * a;
}
}
public static int f(int a, int b) {
return quickPowerMod(a, b-2, b);
}
16.中国剩余定理
中国剩余定理(Chinese Remainder Theorem,CRT)是解决一类同余方程组的经典方法,它可以在同余方程组中减少运算的数量,从而提高计算效率。其公式为:
x ≡ a i m o d m i , i = 1 , 2 , . . . , n x \equiv a_i \bmod m_i,\ i = 1,2,...,n x≡aimodmi, i=1,2,...,n
其中 m 1 , m 2 , . . . , m n m_1,m_2,...,m_n m1,m2,...,mn 两两互质, M = m 1 × m 2 × . . . × m n M = m_1 \times m_2 \times ... \times m_n M=m1×m2×...×mn, M i = M / m i M_i = M/m_i Mi=M/mi。则同余方程组的解为:
x ≡ ( ∑ i = 1 n a i M i x i ) m o d M x \equiv (\sum_{i=1}^{n} a_iM_ix_i) \bmod M x≡(i=1∑naiMixi)modM
其中 x i x_i xi 是 M i M_i Mi 在模 m i m_i mi 意义下的逆元,即 M i x i ≡ 1 m o d m i M_ix_i \equiv 1 \bmod m_i Mixi≡1modmi。
注意,当 m 1 , m 2 , . . . , m n m_1,m_2,...,m_n m1,m2,...,mn 不互质时,同余方程组中不一定存在解,此时需要进行合并或者降维(取模)等操作,转化为互质的情况,然后再使用CRT进行求解。
二、背包问题
1. 01 背包(采用状态压缩)
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int M = scanner.nextInt();
int N = scanner.nextInt();
int[] value = new int[N + 1];
int[] weight = new int[N + 1];
int[] dp = new int[M + 1];
for (int i = 1; i <= N; i++) {
weight[i] = scanner.nextInt();
value[i] = scanner.nextInt();
}
for (int i = 1; i <= N; i++) {
for (int j = M; j >= weight[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
System.out.println(dp[M]);
}
2. 多重背包
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//容量为M
int M = scanner.nextInt();
//种类
int N = scanner.nextInt();
//价值
int[] value = new int[N + 1];
//占用体积
int[] weight = new int[N + 1];
//个数
int[] num = new int[N + 1];
for (int i = 1; i <= N; i++) {
value[i] = scanner.nextInt();
weight[i] = scanner.nextInt();
num[i] = scanner.nextInt();
}
int[] dp = new int[M + 1];
for (int i = 1; i <= N; i++) {
for (int j = M; j >= weight[i]; j--) {
for (int k = 0; k <= num[i] && k * weight[i] <= j; k++) {
dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);
}
}
}
System.out.println(dp[M]);
}
3. 完全背包
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int M = scanner.nextInt();
int[] value = new int[N + 1];
int[] weight = new int[N + 1];
for (int i = 1; i <= N; i++) {
value[i] = scanner.nextInt();
weight[i] = scanner.nextInt();
}
int[] dp = new int[M + 1];
for (int i = 1; i <= N; i++) {
for (int j = weight[i]; j <= M; j++) {
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
}
4.二维背包
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int M = scanner.nextInt();
int V = scanner.nextInt();
int[] weight = new int[N + 1];
int[] volume = new int[N + 1];
int[] value = new int[N + 1];
for (int i = 1; i <= N; i++) {
weight[i] = scanner.nextInt();
volume[i] = scanner.nextInt();
value[i] = scanner.nextInt();
}
int[][] dp = new int[M + 1][V + 1];
for (int i = 1; i <= N; i++) {
for (int j = M; j >= weight[i]; j--) {
for (int k = V; k >= volume[i]; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - weight[i]][k - volume[i]] + value[i]);
}
}
}
System.out.println(dp[M][V]);
}
5.例题1:子集和_可重复
求从一些不重复数字中选取一些数字使得和为target的方案数,每个数字可以重复使用,求方案数。这是一个完全背包问题。
public static void main(String[] args) {
int[] nums = new int[]{0, 1, 2, 3, 4, 5, 6,7,8,9,10};
int target = 12;
int N = 10;
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= N; i++) {
for (int j = nums[i]; j <= target; j++) {
dp[j] += dp[j - nums[i]];
}
}
System.out.println(dp[target]);
}
6.例题2:子集和_不可重复
求从一些不重复数字中选取一些数字使得和为target的方案数,每个数字最多只能选一次,求方案数。这是一个01问题。
public static void main(String[] args) {
int[] nums = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int target = 12;
int N = 10;
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= N; i++) {
for (int j = target; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
System.out.println(dp[target]);
}
7.例题3:2022
从1到2022个数中,选择10个数,求这10个数之和为2022的方案数。这是一个二维背包问题。
public static void main(String[] args) {
long[][] dp = new long[11][2023];
dp[0][0] = 1;
for (int i = 1; i <= 2022; i++) {
for (int j = 10; j >= 1; j--) {
for (int k = 2022; k >= i; k--) {
dp[j][k] += dp[j - 1][k - i];
}
}
}
System.out.println(dp[10][2022]);
}
三、图论
1.邻接表
static class Edge {
int next;
int value;
public Edge(int next, int value) {
this.next = next;
this.value = value;
}
}
static HashMap<Integer, LinkedList<Edge>> graph = new HashMap<>();
public static void addEgde(int from, int to, int value) {
if (!graph.containsKey(from)) {
graph.put(from, new LinkedList<>());
}
if (!graph.containsKey(to)) {
graph.put(to, new LinkedList<>());
}
graph.get(from).add(new Edge(to, value));
graph.get(to).add(new Edge(from, value));
}
2.dijkstra
求单源最短路,不能求带有负权边
import java.util.*;
public class Main{
static class Edge {
int next;
int value;
public Edge(int next, int value) {
this.next = next;
this.value = value;
}
}
static HashMap<Integer, LinkedList<Edge>> graph = new HashMap<>();
//用于记录起始点到每个点的最短距离
static int[] distance;
//用于记录每个点是否被标记过,如果被标记过说明这个点已经是最短路径了
static boolean[] visited;
static int N;
static int M;
public static void addEgde(int from, int to, int value) {
if (!graph.containsKey(from)) {
graph.put(from, new LinkedList<>());
}
if (!graph.containsKey(to)) {
graph.put(to, new LinkedList<>());
}
graph.get(from).add(new Edge(to, value));
graph.get(to).add(new Edge(from, value));
}
public static void dijkstra(int start) {
Arrays.fill(distance, Integer.MAX_VALUE);
Arrays.fill(visited, false);
distance[start] = 0;
int N = graph.size();
for (int i = 0; i < N; i++) {
//1.找到distance的最小值
int min = Integer.MAX_VALUE;
int min_index = -1;
for (int j = 0; j < distance.length; j++) {
if (!visited[j] && min > distance[j]) {
min = distance[j];
min_index = j;
}
}
if (min_index == -1) {
return;
}
//2.标记min_index=true
visited[min_index] = true;
//3.更新从min_index出发的边
LinkedList<Edge> edges = graph.get(min_index);
for (int j = 0; j < edges.size(); j++) {
int next = edges.get(j).next;
int value = edges.get(j).value;
if (!visited[next] && distance[min_index] + value < distance[next]) {
distance[next] = distance[min_index] + value;
}
}
}
//输出结果
System.out.println(Arrays.toString(distance));
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
N = scanner.nextInt();
M = scanner.nextInt();
int start = scanner.nextInt();
for (int i = 0; i < 9; i++) {
int from = scanner.nextInt();
int to = scanner.nextInt();
int value = scanner.nextInt();
addEgde(from, to, value);
}
distance = new int[N + 1];
visited = new boolean[N + 1];
dijkstra(start);
}
}
3.dijkstra优化版本
import java.util.*;
public class Main {
static class Edge {
int next;
int value;
public Edge(int next, int value) {
this.next = next;
this.value = value;
}
}
static class Distance implements Comparable {
int node;
int dis;
public Distance(int node, int dis) {
this.node = node;
this.dis = dis;
}
@Override
public int compareTo(Object o) {
Distance object = (Distance) o;
return Integer.compare(this.dis, object.dis);
}
}
static HashMap<Integer, LinkedList<Edge>> graph = new HashMap<>();
static int N;
static int M;
static int[] distance;
static boolean[] visited;
static PriorityQueue<Distance> queue = new PriorityQueue<>();
public static void addEdge(int from, int to, int value) {
if (!graph.containsKey(from)) {
graph.put(from, new LinkedList<>());
}
if (!graph.containsKey(to)) {
graph.put(to, new LinkedList<>());
}
graph.get(from).add(new Edge(to, value));
graph.get(to).add(new Edge(from, value));
}
public static void dijkstra(int start) {
Arrays.fill(distance, Integer.MAX_VALUE);
distance[start] = 0;
//0.初始优先队列
queue.add(new Distance(start, 0));
while (!queue.isEmpty()) {
//1.从优先队列中弹出优先级最高的,找到最小的distance,然后标记
Distance dis = queue.poll();
if (visited[dis.node]) continue;
int min_index = dis.node;
int min_dis = dis.dis;
visited[min_index] = true;
//2.更新从min发出的所有边
LinkedList<Edge> edges = graph.get(min_index);
for (Edge edge : edges) {
int next = edge.next;
int value = edge.value;
if (!visited[next] && distance[next] > min_dis + value) {
distance[next] = min_dis + value;
//3.压入优先队列
//虽然优先队列中会出现一个点的多个distance,但是后面的用不到
queue.add(new Distance(next, distance[next]));
}
}
}
for (int i = 1; i < distance.length; i++) {
System.out.print(distance[i] + " ");
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
N = scanner.nextInt();
M = scanner.nextInt();
int start = scanner.nextInt();
for (int i = 0; i < M; i++) {
int from = scanner.nextInt();
int to = scanner.nextInt();
int value = scanner.nextInt();
addEdge(from, to, value);
}
distance = new int[N + 1];
visited = new boolean[N + 1];
dijkstra(start);
}
}
4.Floyd
作用:可以用于求带负权边的多源最短路径,并且只能用于邻接矩阵。
Floyd是采用动态规划的思想实现的。
设dp[k][i][j]表示只用前 k 个点时,i 到 j 之间的最短路径是dp[k][i][j]
初始化:dp[0][i][j]表示点与点之间的距离
状态转移:dp[k][i][j] = min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]),注意k是最外层循环
然后进行空间压缩:dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j])
import java.util.*;
public class Main{
static int N;
static int M;
static int[][] distance;
public static void floyd() {
for (int k = 1; k <= N; k++) {
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
distance[i][j] = Math.min(distance[i][j], distance[i][k] + distance[k][j]);
}
}
}
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
System.out.print(distance[i][j] + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
N = scanner.nextInt();
M = scanner.nextInt();
distance = new int[N + 1][N + 1];
//初始化
for (int i = 1; i < N + 1; i++) {
Arrays.fill(distance[i], Integer.MAX_VALUE);
}
for (int i = 1; i <= N; i++) {
distance[i][i] = 0;
}
//输入数据
for (int i = 0; i < N; i++) {
int from = scanner.nextInt();
int to = scanner.nextInt();
int value = scanner.nextInt();
distance[from][to] = value;
distance[to][from] = value;
}
floyd();
}
}
5.SPFA
作用:
1.用于求带负权的边的单元最短路径,但是不能有负环
2.可以用来判断是否存在负环
判断负环:给每一个节点记录一个count,用来记录该结点入队的次数,如果count>n就说明存在负环
import java.util.*;
public class SPFA {
static class Edge {
int next;
int value;
public Edge(int next, int value) {
this.next = next;
this.value = value;
}
}
static int[] distance;
static int[] count;
static HashMap<Integer, LinkedList<Edge>> graph = new HashMap<>();
static int N;
static int M;
public static void addNode(int node) {
if (!graph.containsKey(node)) {
graph.put(node, new LinkedList<>());
}
}
public static void addEdge(int from, int to, int value) {
if (!graph.containsKey(from)) {
addNode(from);
}
if (!graph.containsKey(to)) {
addNode(to);
}
graph.get(from).add(new Edge(to, value));
graph.get(to).add(new Edge(from, value));
}
public static void spfa(int start) {
Arrays.fill(distance, Integer.MAX_VALUE);
distance[start] = 0;
LinkedList<Integer> queue = new LinkedList<>();
queue.add(start);
count[start] = 1;
while (!queue.isEmpty()) {
//1.出队
int node = queue.removeFirst();
LinkedList<Edge> edges = graph.get(node);
//2.遍历所有出边
for (Edge edge : edges) {
int next = edge.next;
int value = edge.value;
//3.如果可以更新则更新
if (distance[next] > distance[node] + value) {
distance[next] = distance[node] + value;
//4.发现没在队列里面就添加进去
if (!queue.contains(next)) {
queue.add(next);
//5.判断是否有负环
count[next]++;
if (count[next] > N) {
System.out.println("存在负环");
return;
}
}
}
}
}
System.out.println(Arrays.toString(distance));
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
N = scanner.nextInt();
M = scanner.nextInt();
for (int i = 0; i < M; i++) {
int from = scanner.nextInt();
int to = scanner.nextInt();
int value = scanner.nextInt();
addEdge(from, to, value);
}
distance = new int[N + 1];
count = new int[N + 1];
spfa(1);
}
}
6.拓扑排序
拓扑排序只能用于有向无环图。如果程序结束,inDegree数字中存在非零元素,说明存在环路。
import java.util.*;
public class Main {
public static HashMap<Integer, LinkedList<Integer>> graph = new HashMap<>();
static int N;
static int M;
static int[] inDegree;
static boolean[] visited;
public static void addEdge(int from, int to) {
if (!graph.containsKey(from)) {
graph.put(from, new LinkedList<>());
}
if (!graph.containsKey(to)) {
graph.put(to, new LinkedList<>());
}
graph.get(from).add(to);
}
public static void topo() {
//如果要求字典序最小,那么可以使用优先队列实现
LinkedList<Integer> queue = new LinkedList<>();
for (int i = 1; i <= N; i++) {
if (inDegree[i] == 0) {
queue.add(i);
}
}
while (!queue.isEmpty()) {
int node = queue.removeFirst();
System.out.print(node+" ");
LinkedList<Integer> edges = graph.get(node);
for (Integer edge : edges) {
inDegree[edge]--;
if (inDegree[edge]==0){
queue.add(edge);
}
}
}
System.out.println(Arrays.toString(inDegree));
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
N = scanner.nextInt();
M = scanner.nextInt();
inDegree = new int[N + 1];
visited = new boolean[N + 1];
for (int i = 0; i < M; i++) {
int from = scanner.nextInt();
int to = scanner.nextInt();
inDegree[to]++;
addEdge(from, to);
}
topo();
}
}
7.欧拉回路与欧拉回路(有向图)
欧拉回路:是指通过图的每一条边恰好一次,且能回到起点的路径。要求起始点和终点一致。
对于一个有向图来说:
①如果每个节点都有入度-出度=0,则该图存在欧拉回路
欧拉路径:是指通过图的每一条边恰好一次,但不要求能回到起点的路径。不要求起始点和终点一致。
对于一个有向图来说:
①如果每个节点都有入度-出度=0,那么该图一定存在欧拉路径;
②如果只有一个节点入度-出度=1,只有一个入度-出度=-1,其余结点入度-出度=0,那么该图一定存在欧拉路径;
注意:如果有欧拉回路,那么求出的欧拉路径就是欧拉回路
public class Main {
static HashMap<Integer, LinkedList<Integer>> graph = new HashMap<>();
static int N;
static int M;
//对于有向图而言
static int[] inDegree;
static int[] outDegree;
//记录欧拉路径的起点,如果有欧拉回路的话,起点无所谓,就当做1
static int oula_start = 1;
static int oula_end = 1;
//记录欧拉路径
//如果有欧拉回路,那么求得的欧拉路径就是欧拉回路
static LinkedList<Integer> path = new LinkedList<>();
static void addEdge(int from, int to) {
if (!graph.containsKey(from)) {
graph.put(from, new LinkedList<>());
}
if (!graph.containsKey(to)) {
graph.put(to, new LinkedList<>());
}
graph.get(from).add(to);
}
static boolean have() {
int count_fu1 = 0;
int count_1 = 0;
boolean result = false;
for (int i = 1; i <= N; i++) {
if (inDegree[i] - outDegree[i] == 1) {
count_1++;
oula_end = i;
} else if (inDegree[i] - outDegree[i] == -1) {
oula_start = i;
count_fu1++;
}
}
if ((count_fu1 == 0 && count_1 == 0)) {
System.out.println("有欧拉回路");
result = true;
} else if (count_fu1 == 1 && count_1 == 1) {
System.out.println("没有欧拉回路,有欧拉路径");
System.out.println("起点为" + oula_start);
System.out.println("终点为" + oula_end);
result = true;
} else {
System.out.println("没有欧拉路径");
}
return result;
}
public static void findPath(int node) {
LinkedList<Integer> edges = graph.get(node);
while (!edges.isEmpty()) {
int next = edges.get(0);
edges.remove(Integer.valueOf(next));
findPath(next);
}
path.add(node);
}
//有向图
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
N = scanner.nextInt();
M = scanner.nextInt();
inDegree = new int[N + 1];
outDegree = new int[N + 1];
for (int i = 0; i < M; i++) {
int from = scanner.nextInt();
int to = scanner.nextInt();
addEdge(from, to);
outDegree[from]++;
inDegree[to]++;
}
//1.判断是否有欧拉路径
have();
//2.求解欧拉路径
//无向图求出的路径可以直接打印,有向图求出的路径需要逆序输出
findPath(oula_start);
//3.判断最后的边数
if (path.size() == M + 1) {
//逆序输出
for (int i = path.size() - 1; i >= 0; i--) {
System.out.print(path.get(i) + " ");
}
} else {
System.out.println("没有欧拉路径");
}
}
}
8.欧拉回路和欧拉路径(无向图)
欧拉回路:是指通过图的每一条边恰好一次,且能回到起点的路径。要求起始点和终点一致
对于一个无向图来说:
①如果每个节点的度数都是偶数,则该图存在欧拉回路;
欧拉路径:是指通过图的每一条边恰好一次,但不要求能回到起点的路径。不要求起始点和终点一致.
对于一个无向图来说:
①如果每个节点的度数都是偶数,那么该图一定存在欧拉路径;
②如果只有两个度数为奇数的节点,那么该图一定存在欧拉路径;
public class Main {
static HashMap<Integer, LinkedList<Integer>> graph = new HashMap<>();
static int N;
static int M;
static int oula_strat = 1;
static int oula_end = 1;
static int[] Degree;
static LinkedList<Integer> path = new LinkedList<>();
static void addEdge(int from, int to) {
if (!graph.containsKey(from)) {
graph.put(from, new LinkedList<>());
}
if (!graph.containsKey(to)) {
graph.put(to, new LinkedList<>());
}
graph.get(from).add(to);
graph.get(to).add(from);
}
public static void have() {
boolean first = true;
int odd_count = 0;
for (int i = 1; i <= N; i++) {
if (Degree[i] % 2 != 0) {
odd_count++;
if (first) {
oula_strat = i;
first = false;
} else {
oula_end = i;
}
}
}
if (odd_count == 0) {
System.out.println("存在欧拉回路");
} else if (odd_count == 2) {
System.out.println("不存在欧拉回路,但存在欧拉路径,起点为:" + oula_strat + ",终点为:" + oula_end);
} else {
System.out.println("不存在欧拉路径");
}
}
static void findPath(int node) {
LinkedList<Integer> edges = graph.get(node);
while (!edges.isEmpty()) {
int next = edges.get(0);
//注意无向图这里,需要删除两次边
edges.remove(Integer.valueOf(next));
graph.get(next).remove(Integer.valueOf(node));
findPath(next);
}
//注意:是在回溯之前加入
path.add(node);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
N = scanner.nextInt();
M = scanner.nextInt();
Degree = new int[N + 1];
for (int i = 0; i < M; i++) {
int from = scanner.nextInt();
int to = scanner.nextInt();
addEdge(from, to);
Degree[from]++;
Degree[to]++;
}
//1.判断是否存在欧拉回路或者欧拉路径
have();
//2.找欧拉回路或者欧拉路径
findPath(oula_strat);
//3.判断最后的边数
if (path.size() == M + 1) {
System.out.println(path.toString());
} else {
System.out.println("没有欧拉路径");
}
}
}
9.最小生成树
import java.util.Arrays;
import java.util.Comparator;
class Edge {
int src, dest, weight;
public Edge(int src, int dest, int weight) {
this.src = src;
this.dest = dest;
this.weight = weight;
}
}
public class KruskalMST {
private int V; // 图中节点的个数
private Edge[] edges; // 图中所有边
// 并查集类
static class UnionFind {
int[] parent, rank;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
}
// 构造函数
public KruskalMST(int V, Edge[] edges) {
this.V = V;
this.edges = edges;
}
// 生成最小生成树
public void kruskal() {
Edge[] result = new Edge[V - 1]; // 最小生成树的边
int e = 0; // 结果数组的索引
int i = 0; // 排序后的边的索引
// 按边权值从小到大排序
Arrays.sort(edges, Comparator.comparingInt(edge -> edge.weight));
// 创建并初始化并查集
UnionFind uf = new UnionFind(V);
// 选择权值最小的边,依次加入最小生成树
while (e < V - 1) {
Edge nextEdge = edges[i++];
int x = uf.find(nextEdge.src);
int y = uf.find(nextEdge.dest);
// 如果加入这条边不会形成环
if (x != y) {
result[e++] = nextEdge;
uf.union(x, y);
}
}
// 输出最小生成树
System.out.println("Edges in the constructed MST:");
for (i = 0; i < e; i++) {
System.out.println(result[i].src + " -- " + result[i].dest + " == " + result[i].weight);
}
}
public static void main(String[] args) {
int V = 4; // 图中节点的个数
Edge[] edges = {
new Edge(0, 1, 10),
new Edge(0, 2, 6),
new Edge(0, 3, 5),
new Edge(1, 3, 15),
new Edge(2, 3, 4)
};
KruskalMST kruskal = new KruskalMST(V, edges);
kruskal.kruskal(); // 执行克鲁斯卡尔算法
}
}
四、高级数据结构
1.树状数组
求前n项和,支持在线修改
public static int getSum(int i) {
int sum = 0;
for (int j = i; j >= 1; j -= lowbit(j)) {
sum += tree[j];
}
return sum;
}
public static void update(int i, int update) {
for (int j = i; j <= n; j += lowbit(j)) {
tree[j] += update;
}
}
public static int lowbit(int num) {
return num & -num;
}
2.ST表
作用是:快速进行区间查询,ST表创建 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n)),查询 O ( 1 ) O(1) O(1),不支持在线修改
public class ST表 {
static int[] arr;
static int n;
static int[][] dp;
//nlog(n)
public static void createST() {
int max = (int) (Math.log(n) / Math.log(2));
dp = new int[n + 1][max + 1];
//自己到自己的最值就是自己
for (int i = 1; i < n; i++) {
dp[i][0] = arr[i];
}
for (int j = 1; j <= max; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
dp[i][j] = Math.max(dp[i][j - 1], dp[i + (1 << (j - 1)) + 1][j - 1]);
}
}
}
//o(1)
public static int query(int l, int r) {
int max = (int) (Math.log(l - r + 1) / Math.log(2));
return Math.max(dp[l][max], dp[r - (1 << max) + 1][max]);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
arr = new int[n + 1];
for (int i = 1; i <= n; i++) {
arr[i] = scanner.nextInt();
}
}
}
3.线段树
//区间和
import java.util.Scanner;
public class Main {
static int[] a;
static int n;
static Node[] tree;
static class Node {
int l;
int r;
int num;
int lazy;//用于记录对于这个节点进行过什么操作
public Node(int l, int r, int num) {
this.l = l;
this.r = r;
this.num = num;
}
}
public static void build(int index, int l, int r) {
if (l == r) {
tree[index] = new Node(l, r, a[l]);
return;
}
int mid = (l + r) / 2;
build(index * 2, l, mid);
build(index * 2 + 1, mid + 1, r);
tree[index] = new Node(l, r, tree[index * 2].num + tree[index * 2 + 1].num);
}
//单点修改
public static void update(int index, int i, int newnum) {
if (tree[index].l == tree[index].r && tree[index].r == i) {
tree[index].num = newnum;
return;
}
int mid = (tree[index].l + tree[index].r) / 2;
if (i <= mid) {
update(index * 2, i, newnum);
} else {
update(index * 2 + 1, i, newnum);
}
tree[index].num = tree[index * 2].num + tree[index * 2 + 1].num;
}
//区间修改:给区间每个值加d
public static void change(int index, int l, int r, int d) {
if (l <= tree[index].l && tree[index].r <= r) {
tree[index].num += (tree[index].r - tree[index].l + 1) * d;
tree[index].lazy += d;
return;
}
spred(index);
int mid = (tree[index].l + tree[index].r) / 2;
if (l <= mid) {
change(index * 2, l, r, d);
}
if (r > mid) {
change(index * 2 + 1, l, r, d);
}
tree[index].num = tree[index * 2].num + tree[index * 2 + 1].num;
}
//一共5个步骤
public static void spred(int index) {
if (tree[index].lazy != 0) {
tree[index * 2].num += (tree[index * 2].r - tree[index * 2].l + 1) * tree[index].lazy;
tree[index * 2 + 1].num += (tree[index * 2 + 1].r - tree[index * 2 + 1].l + 1) * tree[index].lazy;
tree[index * 2].lazy += tree[index].lazy;
tree[index * 2 + 1].lazy += tree[index].lazy;
tree[index].lazy = 0;
}
}
public static int ask(int index, int l, int r) {
if (l <= tree[index].l && tree[index].r <= r) {
return tree[index].num;
}
spred(index);
int sum = 0;
int mid = (tree[index].l + tree[index].r) / 2;
if (l <= mid) {
sum += ask(index * 2, l, r);
}
if (r > mid) {
sum += ask(index * 2 + 1, l, r);
}
return sum;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
a = new int[n + 1];
tree = new Node[n * 4];
int m = scanner.nextInt();
for (int i = 1; i <= n; i++) {
a[i] = scanner.nextInt();
}
build(1, 1, n);
for (int i = 0; i < m; i++) {
int caozuo = scanner.nextInt();
if (caozuo == 1) {
int x = scanner.nextInt();
int y = scanner.nextInt();
int k = scanner.nextInt();
change(1, x, y, k);
} else {
int x = scanner.nextInt();
int y = scanner.nextInt();
System.out.println(ask(1, x, y));
}
}
}
}
4.并查集
public class Main {
// 数组father用于存储每个节点的父节点
static int[] father;
// 数组rank用于存储每个集合的秩(树的高度)
static int[] rank;
// 初始化方法,用于初始化并查集
public static void init(int n) {
father = new int[n];
rank = new int[n];
// 初始化时,每个节点的父节点指向自己,秩为1
for (int i = 0; i < n; i++) {
father[i] = i;
rank[i] = 1;
}
}
// 查找操作,带路径压缩
public static int find(int x) {
if (father[x] == x) {
return x; // 如果x的父节点是自己,返回x
} else {
father[x] = find(father[x]); // 递归查找,并进行路径压缩
return father[x]; // 返回根节点
}
}
// 合并操作,按秩合并
public static void union(int x, int y) {
int father_x = find(x); // 查找x的根节点
int father_y = find(y); // 查找y的根节点
if (father_x == father_y) return; // 如果两个节点的根节点相同,说明已经连通,直接返回
// 按秩合并,将秩小的树挂在秩大的树下
if (rank[father_x] < rank[father_y]) {
father[father_x] = father_y; // x的根节点指向y的根节点
} else if (rank[father_x] > rank[father_y]) {
father[father_y] = father_x; // y的根节点指向x的根节点
} else {
father[father_x] = father_y; // 如果秩相同,随意合并,并增加新根的秩
rank[father_y]++;
}
}
// 主方法,测试并查集功能
public static void main(String[] args) {
init(5); // 创建一个包含5个元素的并查集
union(0, 1); // 合并0和1
union(1, 2); // 合并1和2
System.out.println(find(0) == find(2)); // 输出true,因为0和2是连通的
union(3, 4); // 合并3和4
System.out.println(find(0) == find(4)); // 输出false,因为0和4不连通
}
}
五、动态规划
1.数位dp
public class Main {
static long[] limit;
static int length;
static long[][] dp;
public static long dfs(int pos, int pre, boolean flag, boolean lead) {
if (pos == length) return 1;
if (!flag && !lead && dp[pos][pre] != -1) return dp[pos][pre];
long max = (flag ? limit[pos] : 进制数);
long sum = 0;
for (int i = 0; i <= max; i++) {
if (条件) {
sum += dfs(pos + 1, i, flag && i == limit[pos], lead && i == 0);
}
}
if (!flag && !lead) dp[pos][pre] = sum;
return sum;
}
public static long solve(long num) {
if (num == 0) return 1;
length = ("" + num).length();
limit = new long[length];
dp = new long[length][10];
for (int i = 0; i < length; i++) {
Arrays.fill(dp[i], -1);
}
for (int i = length - 1; i >= 0; i--) {
limit[i] = num % 进制数;
num /= 进制数;
}
return dfs(0, 0, true, true);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int a = scanner.nextInt();
int b = scanner.nextInt();
System.out.println(solve(b) - solve(a - 1));
}
}
2.状态压缩dp
这一部分没有统一的算法模板,只有对二进制操作熟悉之后才能做好这类题,所以我在此总结几个操作。
进行标记:i | (1<<j)
判断是否被标记: (i & (1<<j))==0
判断是否包含:a | b == b
当前行选的元素不能相邻:now & (now >> 1) == 0
当前行与上一行的选择上下也不能相邻:pre & now == 0
注意:添加状态用 | ,删除状态用 ^,判断状态用 &
3.最长公共子序列
public class Main {
public static void main(String[] args) {
char[] str1 = "abcdefgh".toCharArray();
char[] str2 = "acjlfabhh".toCharArray();
int[][] dp = new int[str1.length + 1][str2.length + 1];
for (int i = 1; i <= str1.length; i++) {
for (int j = 1; j <= str2.length; j++) {
if (str1[i - 1] == str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
System.out.println(dp[str1.length][str2.length]);
}
}
4.最大连续子序列和
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{-2, 11, -4, 13, -5, -2};
int[] dp = new int[arr.length];
dp[0] = arr[0];
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
dp[i] = Math.max(dp[i - 1] + arr[i], arr[i]);
max = Math.max(max, dp[i]);
}
System.out.println(max);
}
}
5.最长上升子序列
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{2, 1, 5, 3, 6, 4, 6, 3};
int[] dp = new int[arr.length];
dp[0] = 1;
int max = dp[0];
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0; j--) {
if (arr[i] > arr[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
max = Math.max(max, dp[i]);
}
}
System.out.println(max);
}
}
六、其他
1.二分答案
这是求最小值
while (l < r) {
int mid = (r + l) / 2;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
这是求最大值
while (l < r) {
int mid = (r + l + 1) / 2;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
2.数据离散化
由于原数组的值域很大,所以需要离散化。
原数组:[2,87,98,15,3,1,9999,55,98]
离散化:[2,6,7,4,3,1,8,5,7]
离散化后的数组:存放每个对应位置的数在原数组中排第几,这样每个数对应的大小顺序没有变化,但是值域小了。
public static int[] convert(int[] nums) {
//使用set去重
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
set.add(nums[i]);
}
//排序
Integer[] copy_nums = set.toArray(new Integer[0]);
Arrays.sort(copy_nums);
//进行二分查找:离散化
int[] new_nums = new int[nums.length];
for (int i = 0; i < new_nums.length; i++) {
new_nums[i] = Arrays.binarySearch(copy_nums, nums[i]) + 1;
}
return new_nums;
}
3.求逆序对数(采用树状数组)
public class Main {
public static int[] convert(int[] nums) {
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
set.add(nums[i]);
}
Integer[] copy_nums = set.toArray(new Integer[0]);
Arrays.sort(copy_nums);
int[] new_nums = new int[nums.length];
for (int i = 0; i < new_nums.length; i++) {
new_nums[i] = Arrays.binarySearch(copy_nums, nums[i]) + 1;
}
tree = new int[set.size() + 1];
return new_nums;
}
public static int lowbit(int num) {
return num & -num;
}
public static void update(int index, int value) {
for (int i = index; i < tree.length; i += lowbit(i)) {
tree[i] += value;
}
}
public static long getSum(int index) {
long result = 0;
for (int i = index; i >= 1; i -= lowbit(i)) {
result += tree[i];
}
return result;
}
static int[] tree;
public static void main(String[] args) {
int[] nums = new int[]{1, 2, 3, 4, 5};
int[] new_nums = convert(nums);
int ans = 0;
for (int i = 0; i < new_nums.length; i++) {
update(new_nums[i], 1);
ans += (i + 1) - getSum(new_nums[i]);
}
System.out.println(ans);
}
}
4.康托展开(蓝桥杯例题:排列距离)
import java.util.*;
public class Main {
static char[] arr = new char[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'n', 'o', 'p', 'q', 'r'};
static HashMap<Character, Integer> map = new HashMap<>();
//将字符串转为对应的排列
public static int[] getPailie(String str) {
int[] result = new int[17];
for (int i = 0; i < result.length; i++) {
result[i] = map.get(str.charAt(i));
}
return result;
}
public static long jiecheng(int num) {
if (num == 0) return 1;
if (num == 1) return 1;
return num * jiecheng(num - 1);
}
//获取康托排名
public static long kangtuo(int[] pailie) {
//开始计算排名
long paiming = 1;
for (int i = 0; i < pailie.length; i++) {
int count = 0;//记录有几个比当前的数字小的个数
//注意j是从i的后面找
for (int j = i + 1; j < pailie.length; j++) {
if (pailie[j] < pailie[i]) {
count++;
}
}
paiming += count * jiecheng(pailie.length - i - 1);
}
return paiming;
}
public static void main(String[] args) {
for (int i = 0; i < arr.length; i++) {
map.put(arr[i], i + 1);
}
long A_paiming = kangtuo(getPailie("aejcldbhpiogfqnkr"));
long B_paiming = kangtuo(getPailie("ncfjboqiealhkrpgd"));
long ans = Math.min(B_paiming - A_paiming, jiecheng(17) - B_paiming + A_paiming);
System.out.println(ans);
}
}
5.逆康托展开
public class Main {
static int n = 5;
static long[] jiecheng = new long[n + 1];
static boolean[] used = new boolean[n + 1];
public static String get(long num) {
num--;
StringBuilder result = new StringBuilder();
for (int i = n - 1; i >= 0; i--) {
//从没有确定的数中选择第value+1大的数
int value = (int) (num / jiecheng[i]);
num = num % jiecheng[i];
int count = 0;
for (int j = 1; j <= n; j++) {
if (!used[j]) {
count++;
if (count == value + 1) {
result.append(j);
used[j] =true;
break;
}
}
}
}
return result.toString();
}
public static void getjiecheng(int n) {
jiecheng[0] = 1;
jiecheng[1] = 1;
for (int i = 2; i <= n; i++) {
jiecheng[i] = i * jiecheng[i - 1];
}
}
public static void main(String[] args) {
getjiecheng(n);
System.out.println(get(3));
}
}
6.斐波那契规律
a 1 + . . . + a n = a n + 2 − 1 a_1+...+a_n=a_{n+2}-1 a1+...+an=an+2−1
a 1 2 + . . . + a n 2 = a n ∗ a n + 1 a_1^2+...+a_n^2=a_n*a_{n+1} a12+...+an2=an∗an+1
a 1 + a 3 + . . . + a 2 n − 1 = a 2 n a_1+a_3+...+a_{2n-1}=a_{2n} a1+a3+...+a2n−1=a2n
a 2 + a 4 + . . . + a 2 n = a 2 n + 1 − 1 a_2+a_4+...+a_{2n}=a_{2n+1}-1 a2+a4+...+a2n=a2n+1−1
每一项模一个数的余数是有周期性的
7.坐标变换
一般使用一个数组来进行方向转换
//表示方向
static int[] dx = new int[]{-1, 1, 0, 0};
static int[] dy = new int[]{0, 0, -1, 1};
for (int i = 0; i < 4; i++) {
int new_x = x + dx[i];
int new_y = y + dy[i];
}
8.数列
等差数列:
通项公式: a n = a 1 + ( n − 1 ) d a_n=a_1+(n-1)d an=a1+(n−1)d
前n项目和: S n = n ( a 1 + a n ) 2 = n a 1 + n ( n − 1 ) d 2 S_n=\frac{n(a_1+a_n)}{2}=na_1+\frac{n(n-1)d}{2} Sn=2n(a1+an)=na1+2n(n−1)d
等比数列:
通项公式: a n = a 1 q n − 1 a_n=a_1q^{n-1} an=a1qn−1
前n项目和: S n = a 1 ( 1 − q n ) 1 − q = a 1 − a n q 1 − q S_n=\frac{a_1(1-q^n)}{1-q}=\frac{a_1-a_nq}{1-q} Sn=1−qa1(1−qn)=1−qa1−anq
9.错排数
选出一个数a,可以放的位置有 n − 1 n-1 n−1个,假如放在了b的位置,那么b有如下的两种选择:
①放在a的位置,那么问题就变为剩下的 n − 2 n-2 n−2个元素的错排问题
②如果不放a的位置,那么每个元素可以选的位置都是 n − 2 n-2 n−2个,相当于为 n − 1 n-1 n−1个元素的错排问题
public static int count(int n) {
if (n == 1) return 0;
if (n == 2) return 1;
return (n - 1) * (count(n - 1) + count(n - 2));
}
10.组合数
C 10 3 = 10 × 9 × 8 1 × 2 × 3 {\large C} _{10}^{3} =\frac{10\times 9\times8}{1\times 2\times3} C103=1×2×310×9×8
分子从大到小,分母从小到大
public static long zuhe(long n, long m) {
if (m == 0) return 1;
if (n == m) return 1;
if (m > n / 2) {
m = n - m;
}
long result = 1;
for (long i = n, j = 1; j <= m; i--, j++) {
result = result * i / j;
}
return result;
}
11.相关经验(持续更新)
1.看到最大最小,要想到二分法。
2.看见区间,要想到ST表,线段树。
3.看见前缀和,想到树状数组。
4.多决策问题,首先想到dfs,再看能不能剪枝优化,如果能套上dp,就用dp。
希望这篇博客文章对你有所帮助!