递归的含义很好理解,就是一个函数调用自身,难就难在如何确定一个题目的递归式–这就需要多刷题了
一个完整的递归函数包括两个部分
- 1.递归式
- 2.递归入口
以斐波那契数列为例:
int f(int n){
if(n1||n2) return 1; //出口
return f(n-1)+f(n-2); //递归式
}
递归式用来递归计算我们想要得到的值,递归出口用来结束递归
子问题和原文题的求解方式完全相同的时候可以用递归
蓝桥杯练习习通 计算n阶行列式公式
计算n阶行列式
给定一个N×N的矩阵A,求|A|。
输入格式:
第一行一个正整数N。 接下来N行,每行N个整数,
第i行第j个数字表示A[i][j]。
输出格式 一行,输出|A|。
A11A12…A1n
A21A22…A2n
…
An1An2…Ann
n==2 : ∣ A ∣ = A 11 ∗ A 22 − A 12 ∗ 21
A B C
D E F = A * E F + B * D F + C * D E
G H I H I G I G H
所以当n == 2时就是递归出口了, n > 2 递归计算。
import java.io.*;
public class 计算行列式{
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
public static int Int(String s){return Integer.parseInt(s);}
public static void copy(int[][]A, int[][] A1, int i, int len) throws IOException{
for(int x = 1; x < len; x++)
for(int y = 0, j = 0; j < len; j++)
if(j != i) {
A1[x-1][y++] = A[x][j];
}
}
public static int F(int[][] A, int len)throws Exception{
int res = 0; //保存每个矩阵的|A|
if(len == 1)return A[0][0];
if(len == 2){
return A[0][0]*A[1][1] - A[0][1]*A[1][0]; // 递归出口
}
else{
int A1[][] = new int[10][10];
for(int i = 0; i < len; i++){
copy(A, A1, i, len);
res += Math.pow(-1, i) * A[0][i] * F(A1, len-1); //递归式
}
}
return res;
}
public static void main(String[] args) throws Exception{
int n;
n = Integer.parseInt(in.readLine());
int arr[][] = new int[10][10];
for(int i = 0; i < n; i++){
String[] s = in.readLine().split(" ");
for(int j = 0; j < n; j++){
arr[i][j] = Int(s[j]);
}
}
out.write(F(arr, n) + "\n");
out.flush();
}
}
DFS和BFS是两种搜索树和图的基本策略
DFS常用于暴力搜索所有状态
BFS常用于搜索到某一状态的最短路径
我们将DFS和BFS搜搜的东西称为状态,状态就是一个具体问题的解,而将所有状态关联在一起,就能得到一个状态图,DFS和BDF就是遍历状态图的两种方式
位运算又称为位操作,指的是直接对二进制进行的一系列操作
1.AND(&) 按位与
2.| 按位或
3.按位异或
4.取反(~)
5.移位运算
1.(n>>k) &1 取出整数n在二进制表示下的第k位
2.n & ((1 << k) - 1) 取出整数n在二进制表示下的第0~k-1位(后k位)
3.n ^ (1 << k) 把整数n在二进制表示下的第k位取反
4.n | (1 << k) 把整数n在二进制表示下的第k位赋值为1
5.n & (~(1 << k)) 把整数n在二进制表示下的第k位赋值为0
6.n ^ (1 << k) = n - (1<
7.除以2
a / 2 = a >> 1
(a + b) / 2 == a + b >> 1 ( + - 运算的优先级高于 <<, >> )8.判断奇偶
一个数的二进制数的最低位如果是1 则该数一定是奇数 否则一定是偶数
所以 用 a & 1 检测最低为是否位1if(a & 1) cout<<"奇数"; else cout<<"偶数"
- 状态压缩以一个二进制数表示一个状态集合。
如 n = 1100 S = {2, 3} S表示状态所有为1的集合。- 成对变换当n 为偶数时 n ^ 1 = n + 1
当n为奇数时 n ^ 1 = n - 1
所以
(0,1) (2, 3) (4, 5)… 关于 ^1 运算 构成“成对变换”
这一性质常用于图论邻接表中边集的存储。在具有无向边(双向边)的图中把一对正反方向的边分别存储在邻接表数组中的第n和第n+1位置(n为偶数),就可以通过^1
的运算获得与当前边(x, y) 反向的边(y, x)的存储位置。
本文主要介绍二分法。 二分法是一种精妙算法,效率高
二分法用在于单调的序列内快速查找某个值,方法是序列分为两半,判断要查找的值在那个区间,舍弃另一半
在有序的序列中查找x是否存在
另外一种解释方式就是在序列中可以正确插入X而不影响序列升序状态的第一个位置
public static int Search(int x){
int l = 0, r = n-1;
while(l < r){
int mid = l + r >> 1;
// mid位置上的元素小于x,我们要查找的是大于等于的
if(arr[mid] < x)
{
// 所以mid和其左边的全部舍弃
l = mid + 1;
}
else
{
//mid位置上的元素大于等于x,我们不能舍弃,但要向左偏移,所以令r=mid
r = mid;
}
}
return l;
}
public static int Search(int x){
int l = 0, r = n-1;
while(l < r){
int mid = l + r + 1 >> 1; // 注意这里加了1
//+1是为了在二分的时候向下取整的尾数不为1
// mid位置上的元素大于x,我们要查找的是小于等于的
if(arr[mid] > x)
{
// 所以mid和其右边的全部舍弃
r = mid - 1;
}
else
{
//mid位置上的元素小于等于x,我们不能舍弃,但要向右偏移,所以令l=mid
l = mid;
}
}
return l;
}
在以上的代码中, mid = (l + r + 1) / 2
为什么呢? 因为(l+r)/2的结果会被下取整,当l = 0, r = 1时,mid = 0如果此时arr[0]正好等于x的话, l 就会等于0, 那么因为l和r并没有被更新,
下一次循环mid还是0, 所以就会一直这样循环下去。为了避免死循环,需要特殊处理一下边界问题,也就是(l + r + 1) / 2。这样得到的mid就会不同了。
import java.io.*;
import java.util.*;
public class Main{
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static String[] s = new String[100010];
static int[] arr = new int[100010];
static int n, q;
public static int findl(int x){
int l = 0, r = n-1;
while(l < r){
int mid = l + r >> 1;
if(arr[mid] < x) l = mid + 1;
else r = mid;
}
return l;
}
public static int findr(int x){
int l = 0, r = n-1;
while(l < r){
int mid = l + r + 1 >> 1;
if(arr[mid] > x) r = mid - 1;
else l = mid;
}
return l;
}
public static void main(String[] agrs) throws IOException {
String[] test = in.readLine().split(" ");
n = Integer.valueOf(test[0]);
q = Integer.valueOf(test[1]);
s = in.readLine().split(" ");
for(int i = 0; i < n; i++) arr[i] = Integer.parseInt(s[i]);
for(int i = 0; i < q; i++){
int x = Integer.valueOf(in.readLine());
int l = findl(x);
int r = findr(x);
if(x != arr[l]) out.write("-1 -1\n"); //不存在
else out.write(l + " " + r + "\n");
}
out.flush();
}
}
从序列中取一个基准值,然后将序列分为左右两部分,使得左边的数比基准值小,右边的数比基准值大。递归这个过程,直到不可再分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHOAx2uB-1620523087785)(C:%5CUsers%5C%E4%B8%BF%E5%89%91%E6%9D%A5%C2%B7%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20210415211131275.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AApEBLak-1620523087787)(C:%5CUsers%5C%E4%B8%BF%E5%89%91%E6%9D%A5%C2%B7%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20210415211430021.png)]
import java.io.*;
import java.util.*;
public class 快速排序{
static int[] arr = new int[100010];
public static void q_sort(int l,int r){
if(l>=r)return;
int i = l - 1;
int j = r + 1;
int x = arr[l + r >> 1];
while(i < j){
do i++;while(arr[i] < x);
do j--;while(arr[j] > x);
if(i<j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
q_sort(l,j);
q_sort(j+1,r);
}
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(in.readLine());
String s[] = in.readLine().split(" ");
for(int i = 0; i < s.length; i++){
arr[i] = Integer.parseInt(s[i]);
}
q_sort(0, n-1);
for(int i = 0; i < n; i++){
System.out.print(arr[i]+" ");
}
}
}
我们知道链表是通过指针将所有的结点链接实现的,在此过程中,没创建的一个新的接待你,都要给他分配空间,而且要new一下,这个非常耗时,另外,链表用代码实现是很麻烦的
我们可以将数组每个元素当作一个节点,然后当我们呢开辟一个长度为n的数组我们就得到了n个未使用的节点,用数组模拟的链表也被称为静态链表。
怎么实现呢
1.首先你需要一个头指针head。因为用数组模拟,所以我们只知道数组下标就能知道该位置的值为多少。所以head用int存储。
int head =-1; //刚开始指向为空
2.链表的一个节点需要两个数据:值和next指针,而数组的一个位置只能存 储一个数据,所以,我们还需要两个数组,一个存储值,一个存储 next指针。
int[] data = new int[100010];
int[] next = new int[100010];
3.因为我们需要能够从头结点依次找到所有的结点,所以我们需要给每个结点设置一个编号,这样我们就可以根据编号找到每个结点(相当于地址)。
int idx = 0; // 从数组的0下标开始存储。
4.然后最关键的一步就是插入了,这里我们采用头插法:和指针实现的链表一样,我们需要先把值给存到结点中:
data[idx] = X; // 将X存到data[idx]中
5.然后我们需要将新插入的结点的next指针指向头结点指向的结点。
next[idx] = head; // 注意这里的head存的是头结点下一个结点的地址,相当于head.next
public static void add_head(int x){
data[idx] = x;
next[idx] = head;
head = idx ++;
}
public static void delete(int x){
for(int i = head, j = next[head]; i != -1; i = next[i]){
if(data[i] == x){
// do something you like
}
}
}
本文主要介绍哈希表的定义,并用数组模拟哈希表
哈希表又称为散列表,其原理是每个元素都能通过哈希函数得到一个哈希值,然后改元素在表中存储的下表就是该哈希值,访问的时候也是通过哈希值进行访问的。理论查找时间复杂度是O(1)
一般的整数哈希函数: hash(X) = (x%P+P)%P
P一般取大于最大数据量的第一个质数
举个例子:
有一组数,个数为 8,1 3 5 7 13 20 50 101 ,则P取11, 11是大于8的第一个质数。
然后分别求出这8个数的哈希值。
hash(1) = 1
hash(3) = 3
hash(5) = 5
hash(7) = 7
hash(13) = 2
hash(20) = 9
hash(50) = 4
hash(101) = 2
我们发现以上哈希值又两个相同的,101和13放在了同一个位置,这被称为冲突
将数据储存在哈希值对应的单链表中
“I x”,插入一个数x;
“Q x”,询问数x是否在集合中出现过;
现在要进行N次操作,对于每个询问操作输出对应的结果。输入格式
第一行包含整数N,表示操作数量。
接下来N行,每行包含一个操作指令,操作指令为”I x”,”Q x”中的一种。
输出格式
对于每个询问指令“Q x”,输出一个询问结果,如果x在集合中出现过,则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1≤N≤1051≤N≤105
−109≤x≤109−109≤x≤109
输入样例:5
I 1
I 2
I 3
Q 2
Q 5
输出样例:Yes
No
代码:
import java.io.*;
import java.util.*;
public class Main{
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static final int N = 100003; // 假设数据最多为100000个
static int[] hash = new int[N]; //哈希表表头
static int[] data = new int[N]; // 所有单链表共用一个数组空间
static int[] next = new int[N]; // next指针
static int idx = 1; // static标识的数组是自动初始化为0的所以让
//结点的编号从 1 开始,这样就不需要对hash初始化为-1了,当指针为0时,则表示为空。
public static void Insert(int x){
int k = (x % N + N) % N;
data[idx] = x;
next[idx] = hash[k];
hash[k] = idx;
idx++;
}
public static void find(int x) throws IOException{
int k = (x % N + N) % N;
k = hash[k];
Boolean flag = false;
while(k != 0){
if(data[k] == x){
flag = true;
break;
}
k = next[k];
}
if(flag){
out.write("Yes\n");
}
else out.write("No\n");
out.flush();
}
public static void main(String[] agrs)throws IOException {
int n = Integer.parseInt(in.readLine());
for(int i = 0; i < n; i++){
String s[] = in.readLine().split(" ");
switch(s[0]){
case "I" : Insert(Integer.parseInt(s[1])); break;
case "Q" : find(Integer.parseInt(s[1])); break;
}
}
}
}
本文主要介绍并查集和其效率最高并且最简单的实现方法
并查集顾名思义,是一种用于处理集合和集合至今啊查询和合并等·操作的数据结构,比如询问两点是否在同一集合,将两个不同集合合并
给出N个点划分为两个集合,查询其中某两个点是否在同一集合内
简单的方法就是,将同一个集合内的所有点设置一个标记,然后查询两个点标记是否一样 ——这就是并查集
1.首先,开一个数组,存n个点
int p[N];
2.刚开始每个点都在不同集合内,所以初始化让每个点指向自己
for(int i = 1; i <= n; i++) p[i] = i; //并查集初始化
3.那么让两个不用的集合合并呢? 很简单但,让其中一个带你指向另一个点
p[a] = b; // 将a连接到b所在的集合内
或者
//p[b] = a; //将b连接到a所在的集合内
4.那么问题来了,直接让p[a] = b, 那查询的时候肯定会出错的,因为每个点所指向的点都不一样,前面说了,要将同一个集合内的点指向同一个标记**,所以p[a] 指向的点应该是b的标记,也就是p[b]的父节点。**
也就是p[a] = p[b];但是如果b指向不是那个标记,也就是最终的父节点,这时就又出错了。所以我们要让每个点都指向最终的父结点,也就是那个标记。所以我们要先找到父节点。某一个点指向自己,那么它就是父节点,其他点均指向它。
此find函数是并查集的精髓所在
public static int find(int x){
if(p[x] != x) p[x] = find(p[x]);// p[x] != x 说明p[x]指向的不是父节点
return p[x]; // 在搜索父节点的同时更新p[x]让其指向父节点
}
这时应该让4指向3 而不是否则查询2,4是否在同一个集合肯定会出错的
所以,find很重要,不能写错
本文主要介绍邻接矩阵和邻接表的实现方式,无向图和有向图的区别,以及稠密图和稀疏图的区别。以及两个存储方式的使用场景。稠密图使用邻接矩阵存储,稀疏图使用邻接表存储
无向图是特殊的有向图 无向图就是两个点只要有边就可以相互到达
稀疏图一般指的是一个图中点数和边数是同一数量级的,比如点数是1000,边数也是1000左右的,而稠密图一般指边数是点数的平方,比如点数是100,而边数是10000。
说白了就是一个数组
那么邻接矩阵D表示为:
其中INF表示正无穷,即该店和另外一个点没有边相连
所以 D[i][j]就是i号点到j号点之间边的长度
因为自己到自己的距离是0,并且如果两点之间没有边相连要设为INF,所以要先将这两种情况给初始化。
假设有一共有n个点,设邻接矩阵为D
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; i++){
if(i == j){
D[i][j] = 0;
}
else{
D[i][j] = INF;
}
}
}
注意: INF一般设为:0x3f3f3f3f
重边和自环:
重边就是输入数据中重复给出两个点之间的边
自环就是一个点与自己项链
无向图和有向图的区别:
无向图是特殊的有向图,我们对有向图建边的时候只需要建立一条边,无向图则需要建立正反两条边
有些题目可能存在重边和自环,所以读入边的时候要特判一下。比如求最短路问题时会将最小的边存储起来。
一般的读入格式 : (假设一共有m条边,每行给出3个整数,a,b,w表示a到b的边长为w)
邻接表采用的是数组+链表的存储方式,和之前讲过的哈希表用拉链法实现的方式是一模一样的。
图示为:
所以邻接表就是将一个点的所有出边存储在一条单链表上。
邻接表的实现方法有很多种,可以使用二维的Vector(动态数组)来存储每个点的出边。
另外,也可以使用数组模拟单链表的方法,这里推荐使用数组模拟单链表的方法来实现邻接表,原因还是这种方式效率高。
另外,这跟哈希表用拉链法解决冲突是完全一样的。唯一的区别是,再存储边的时候,我们不仅需要存储边的长度还需要存储当前点是和哪个点相连。
另: (静态邻接表又被称为链式前向星)
int [] e = new int[N]; // 表示指向的点
int [] w = new int[N]; // 表示边长
int [] next = new int[N]; // next指针
int [] head = new int[N]; // 邻接表表头
int idx = 1; // 链表结点编号
// 插入
public static void add(int a, int b, int c){ // 将a和b之间建一条边
e[idx] = b; // 存储a点指向哪个点
w[idx] = c; // 存储边长
next[idx] = head[a];
head[a] = idx++;
}
// 遍历一个点的所有出边
for(int i = head[a]; i != 0; i = next[i]){
int b = e[i]; // a点指向的边
int c = w[i]; // a到b的边长
}
// 无向图的读入, 假如有m条边
for(int i = 0; i < m; i++){
int a = in.nextInt();
int b = in.nextInt();
int c = in.nextInt();
add(a, b, c);
add(b, a, c); //因为是无向图所以要反向建边。
}
研究最短路的五种算法:
无负边权时适用算法(所有边长都为正数)
有负边权适用算法:
求有变数限制的单源最短路:
存储稠密图时,使用邻接矩阵。
存储稀疏图时,使用邻接表。
稠密图是指边数远大于点数,稀疏图是边数和点数差不多相等。
以n表示点数, m表示边数,
一般来说题目数据是n < 100, m < 10000 的图是稠密图, 是n<10000, m<10000的图是稀疏图。
算法思想:
朴素DijKastra的思想是通过一个距离七点A最近的点B,缩短七点A通过B到达其他点的距离,因为只有通过距离七点最近的点,才有可能缩短起点到达其他店的距离
所以一共分两步
第一步: 找到距离起点最近的点
第二步:通过该点缩短起点到达其他店的距离
对于AB和AC,AB为3,AC为6,AB是A到其他店的最短距离,如果AC有可能更小的话,那么一定是经过B。原本A到C是6, 但经过B, ABC的距离是4.所以AC的最短距离是4.
实现:
1.我们设置一个dis数组,来表示其他点到起点的最短距离.起点到起点的距离是0,所以dis[1]=0,其他店到起点的距离为正无穷
2.然后,我们要找到一个距离起点最近点.用循环去遍历
int min = INF //表示没有处理过距离最近的点
//找出距离起点最近的点
for(int j=1;j<=n;j++){
if(st[j]==0&&st[min]>st[j]){
min = j;
}
}
所以min就是我们找到的还没处理过的距离起点最近的点
处理的意思就是已经确定该点的最短路径
然后通过min这个点去缩点起点到达其他点的距离
for(int j =1; i<=n; j++){
dis[j] = max(dis[j],dis[j]+w[min][j])
}
核心代码:
public static int Dijkstra(){
Arrays.fill(dis, 0x3f3f3f3f);
dis[1] = 0;
for(int i = 0; i < n; i++){ // 循环n次, 每次找出一个离起点最近的点。
int t = -1;
for(int j = 1; j <= n; j++){
if(S[j] == 0 && (t == -1 || dis[t] > dis[j])){ // 找到一个距离最短的加入S集合。
t = j;
}
}
S[t] = 1; // 然后通过这个点来缩短原点到达其他点的距离。
for(int j = 2; j <= n; j++){
dis[j] = Math.min(dis[j], dis[t] + w[t][j]);
}
}
if(dis[n] != 0x3f3f3f3f) return dis[n];
else return -1;
}
我们只需要判断[2,根号n] 内是否含有n的银子,如果有则n为合数,否则,则为质数
public static Boolean isprime(int n){
if(n == 1) return false;
for(int i = 2; i <= n / i; i++){
if(n % i == 0){
return false;
}
}
return true;
}
根据算术基本定理又称唯一分解定理,对于任何一个合数, 我们都可以用几个质数的幂的乘积来表示。
即: N = p 1 k 1 ∗ p 2 k 2 ∗ . . . ∗ p n k n N = p_1{k_1}*p_2{k_2}*… *p_n^{k_n}N=p
如:
12 = 2 2 ∗ 3 12 =2^2312=2
2
∗3
20 = 2 2 ∗ 5 20 = 2^2520=2
2
∗5
30 = 2 ∗ 3 ∗ 5 30 = 23530=2∗3∗5
接下来我们利用这个公式分解质因数。
设一个质数为p.如果n%p == 0
,那么p就是n的一个质因数,接下来就是求p的指数,我们让n = n/p
, 这样就从n中剔除了一个p,接着重复上述两步,直到n%p != 0
埃氏筛法 欧拉筛法
现在求区间[1,e7]所有质数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ljy5p19b-1620523087790)(C:%5CUsers%5C%E4%B8%BF%E5%89%91%E6%9D%A5%C2%B7%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20210417110937273.png)]