基础算法
快速排序
#include<iostream> using namespace std; const int N = 1000010; int q[N],n; void quick_sort(int q[],int l,int r){ if(l>=r)return; int i = l-1,j = r+1; int x=q[l+r>>1]; while(i<j){ do i++;while(q[i]<x); do j--;while(q[j]>x); if(i<j)swap(q[i],q[j]); } quick_sort(q,l,j); quick_sort(q,j+1,r); } int main(){ scanf("%d",&n); for(int i = 0;i<n;i++)scanf("%d",&q[i]); quick_sort(q,0,n-1); for(int i = 0;i<n;i++)printf("%d ",q[i]); return 0; }
归并排序
#include<iostream> using namespace std; const int N = 100010; int n,q[N],tmp[N]; void merge_sort(int q[],int l,int r){ if(l>=r)return; int mid = l+r>>1; merge_sort(q,l,mid),merge_sort(q,mid+1,r); int k = 0,i = l,j = mid+1; while(i<=mid&&j<=r) if(q[i]<q[j])tmp[k++] = q[ i++]; else tmp[k++] = q[j++]; while(i<=mid)tmp[k++] = q[i++]; while(j<= r)tmp[k++] = q[j++]; for(int i =l,j =0 ;i<=r;i++,j++)q[i] = tmp[j]; } int main(){ scanf("%d",&n); for(int i = 0;i<n;i++)scanf("%d",&q[i]); merge_sort(q,0,n-1); for(int i =0;i<n;i++)printf("%d ",q[i]); return 0; }
并查集大致讲解
开始时每个集合都是一个独立的集合,并且都是等于自己本身下标的数例如:p[5]=5,p[3]=3;
如果是M操作的话那么就将集合进行合并,合并的操作是:p[3]=p[5]=5;
所以3的祖宗节点便成为了5
此时以5为祖宗节点的集合为{5,3}
如果要将p[9]=9插入到p[3]当中,应该找到3的祖宗节点,
然后再把p[9]=9插入其中,所以p[9]=find(3)
;(find()函数用于查找祖宗节点)
也可以是p[find(9)]=find(3),因为9的节点本身就是9此时以5为祖宗节点的集合为{5,3,9};
如果碰到多个数的集合插入另一个集合当中其原理是相同的
例如:
上述中以5为祖宗节点的是p[5],p[3],p[9];(即p[5]=5,p[3]=5,p[9]=5
)
再构造一个以6为祖宗节点的集合为{6,4,7,10}
如果要将以6为祖宗节点的集合插入到以5为祖宗节点的集合,则该操作可以是
p[6]=find(3)
(或者find(9),find(5))
此时p[6]=5
当然如果是以6为祖宗节点集合中的4,7,10则可以这样
p[find(4)]=find(3)
或者p[find(7)]=find(3)
均可以
此时以6为祖宗节点的集合的祖宗节点都成为了5
#include<iostream> using namespace std; const int N = 100010; int n,m; int p[N]; int find(int x){ //返回x的祖宗节点 + 路径压缩 if(p[x]!=x)p[x] = find(p[x]); return p[x]; //p[x]表示x的父亲节点 } int main(){ int n,m; scanf("%d%d",&n,&m); for(int i = 0;i<=n;i++)p[i] = i; while(m--){ char op[2]; int a,b; scanf("%s%d%d",op,&a,&b); if(*op == 'M')p[find(a)] = find(b);//a的祖宗节点的父亲节点为b的祖宗节点 else{ if(find(a) == find(b))puts("Yes"); else puts("No"); } } return 0; }
高精度加法
#include<iostream> using namespace std; const int N = 100010; int A[N],B[N],C[N]; int add(int a[],int b[],int c[],int cnt){ int t = 0; for(int i = 1;i<=cnt;i++){ t+=a[i]+b[i]; c[i] = t%10; t/=10; } if(t)c[++cnt] = 1; return cnt; } int main(){ string a,b; cin>>a>>b; int cnt1= 0; for(int i = a.size()-1;i>=0;i--){ A[++cnt1] = a[i] - '0'; } int cnt2 = 0; for(int i = b.size()-1;i>=0;i--){ B[++cnt2] = b[i] - '0'; } int tot = add(A,B,C,max(cnt1,cnt2)); for(int i = tot;i>=1;i--){ cout<<C[i]; } return 0; }
前缀和
什么是前缀和?数列的和时,Sn = a1+a2+a3+…an; Sn就是数列的前 n 项和。
前缀和就是新建一个数组,新建数组中保存原数组前 n 项的和。
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式共 m 行,每行输出一个询问的结果。
数据范围1≤l≤r≤n,1≤n,m≤100000,−1000≤数列中元素的值≤1000输入样例:
5 3 2 1 3 6 4 1 2 1 3 2 4
输出样例:
3 6 10
#include<iostream> using namespace std; const int N = 100010; int a[N],s[N]; int main(){ ios::sync_with_stdio(false); int n,m; cin>>n>>m; for(int i = 1;i<=n;i++)cin>>a[i]; s[0] = 0; for(int i = 1;i<=n;i++)s[i] = s[i-1]+a[i]; while(m--){ int l,r; cin>>l>>r; cout<<s[r] - s[l-1]<<endl; } return 0; }
二维前缀和推导
如图:
紫色面积是指(1,1)左上角到(i,j-1)右下角的矩形面积, 绿色面积是指(1,1)左上角到(i-1, j )右下角的矩形面积。每一个颜色的矩形面积都代表了它所包围元素的和。从图中我们很容易看出,整个外围蓝色矩形面积s[i][j] = 绿色面积s[i-1][j] + 紫色面积s[i][j-1] - 重复加的红色的面积s[i-1][j-1]+小方块的面积a[i][j];
因此得出二维前缀和预处理公式
s[i] [j] = s[i-1][j] + s[i][j-1 ] + a[i] [j] - s[i-1][ j-1]
接下来回归问题去求以(x1,y1)为左上角和以(x2,y2)为右下角的矩阵的元素的和。
如图:
紫色面积是指 ( 1,1 )左上角到(x1-1,y2)右下角的矩形面积 ,黄色面积是指(1,1)左上角到(x2,y1-1)右下角的矩形面积;
不难推出:
绿色矩形的面积 = 整个外围面积s[x2, y2] - 黄色面积s[x2, y1 - 1] - 紫色面积s[x1 - 1, y2] + 重复减去的红色面积 s[x1 - 1, y1 - 1]
因此二维前缀和的结论为:
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]
#include<iostream> using namespace std; const int N = 1010; int n,m,q; int s[N][N]; int main(){ cin>>n>>m>>q; for(int i = 1;i<=n;i++){ for(int j = 1;j<=m;j++){ cin>>s[i][j]; } } for(int i = 1;i<=n;i++){ for(int j = 1;j<=m;j++){ s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1]; } } while(q--){ int x1,x2,y1,y2; cin>>x1>>y1>>x2>>y2; cout<<s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1 - 1][y1 - 1]<<endl; } return 0; }
模拟栈
实现一个栈,栈初始为空,支持四种操作:
push x
– 向栈顶插入一个数 x;pop
– 从栈顶弹出一个数;empty
– 判断栈是否为空;query
– 查询栈顶元素。现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示栈顶元素的值。
数据范围1≤M≤100000,1≤x≤109所有操作保证合法。
输入样例:
10 push 5 query push 6 pop query pop empty push 4 query empty
输出样例:
5 5 YES 4 NO
#include<iostream> using namespace std; const int N=100010; int stk[N],top;//stk[]为栈,top为栈顶。 int main(){ int m; cin>>m; while(m--){ string op; int x; cin>>op; if(op=="push"){//向栈顶插入一个元素 cin>>x; stk[top++]=x;// } else if(op =="pop"){//从栈顶弹出一个数 --top; }else if(op == "empty")//判断栈是否为空 if(top)printf("NO\n");//查询栈顶元素 else printf("YES\n"); else cout<<stk[top-1]<<endl; } return 0; }
模拟队列
实现一个队列,队列初始为空,支持四种操作:
push x
– 向队尾插入一个数 x;pop
– 从队头弹出一个数;empty
– 判断队列是否为空;query
– 查询队头元素。现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示队头元素的值。
数据范围1≤M≤100000,1≤x≤109,所有操作保证合法。
输入样例:
10 push 6 empty query pop empty push 3 push 4 pop query push 6
输出样例:
NO 6 YES 4
#include<iostream> using namespace std; const int N = 1e5+10; int q[N]; int main(){//队列的顶部在下面,底部在上面 int m; int hh=0,tt=-1;//hh为队列的顶部,tt为队列的底部。 cin>>m; while(m--){ string op; int x; cin>>op; if(op == "push"){ cin>>x; q[++tt] = x;//在队列的底部插入元素 }else if(op == "pop"){ hh++;//将元素从队列的底部弹出 }else if(op == "empty"){ if(hh<=tt)cout<<"NO"<<endl;//如果顶部指针小于等于底部指针,说明不空 else cout<<"YES"<<endl; }else { cout<<q[hh]<<endl;//查询操作,输出队头元素。 } } return 0; }
单调栈
算法原理:用单调递增栈,当该元素可以入栈的时候,栈顶元素就是它左侧第一个比它小的元素。以:3 4 2 7 5 为例,过程如下:
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式第一行包含整数 N,表示数列长度。
第二行包含 N 个整数,表示整数数列。
输出格式共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围1≤N≤1051≤数列中元素≤109输入样例:
5 3 4 2 7 5
输出样例:
-1 3 -1 2 2
#include<iostream> using namespace std; const int N = 100010;//栈是一个上面开口的长方体, int stk[N],tt;//tt为栈的顶部,即开口的部分 int main(){ int n; cin>>n; while(n--){ int x; cin>>x; while(tt && stk[tt]>=x)tt--;//当栈顶元素即tt存在,不为空,并且,当前栈顶元素大于等于待入栈的元素x,则当前栈顶元素出栈。 if(!tt)cout<<"-1 ";//如果栈空,则说明没有比当前栈顶元素小的值,输出-1. else cout<<stk[tt]<<" ";//如果栈不空,则当前栈顶元素就是第一个比它小的元素。输出栈顶元素。 stk[++ tt] = x;//带入元素入栈,下一轮判断中,它即为当前栈顶元素。 } return 0; }
KMP
一、什么是KMP算法及一些基本概念首先,什么是KMP算法。这是一个字符串匹配算法,对暴力的那种一一比对的方法进行了优化,使时间复杂度大大降低(我不会算时间复杂度。。。,目前也只能这么理解,还有KMP是取的三个发明人的名字首字母组成的名字)。
然后是一些基本概念:
1、s[ ]是模式串,即比较长的字符串。2、p[ ]是模板串,即比较短的字符串。(这样可能不严谨。。。)3、“非平凡前缀”:指除了最后一个字符以外,一个字符串的全部头部组合。4、“非平凡后缀”:指除了第一个字符以外,一个字符串的全部尾部组合。(后面会有例子,均简称为前/后缀)5、“部分匹配值”:前缀和后缀的最长共有元素的长度。6、next[ ]是“部分匹配值表”,即next数组,它存储的是每一个下标对应的“部分匹配值”,是KMP算法的核心。(后面作详细讲解)。
核心思想:在每次失配时,不是把p串往后移一位,而是把p串往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数的失配步骤。而每次p串移动的步数就是通过查找next[ ]数组确定的。
二、next数组的含义及手动模拟(具体求法和代码在后面) 然后来说明一下next数组的含义:对next[ j ] ,是p[ 1, j ]串中前缀和后缀相同的最大长度(部分匹配值),即 p[ 1, next[ j ] ] = p[ j - next[ j ] + 1, j ]。
如:
手动模拟求next数组:
对 p = “abcab”
p a b c a b下标 1 2 3 4 5next[ ] 0 0 0 1 2对next[ 1 ] :前缀 = 空集—————后缀 = 空集—————next[ 1 ] = 0;
对next[ 2 ] :前缀 = { a }—————后缀 = { b }—————next[ 2 ] = 0;
对next[ 3 ] :前缀 = { a , ab }—————后缀 = { c , bc}—————next[ 3 ] = 0;
对next[ 4 ] :前缀 = { a , ab , abc }—————后缀 = { a . ca , bca }—————next[ 4 ] = 1;
对next[ 5 ] :前缀 = { a , ab , abc , abca }————后缀 = { b , ab , cab , bcab}————next[ 5 ] = 2;
三、匹配思路和实现代码 KMP主要分两步:求next数组、匹配字符串。个人觉得匹配操作容易懂一些,疑惑我一整天的是求next数组的思想。所以先把匹配字符串讲一下。
s串 和 p串都是从1开始的。i 从1开始,j 从0开始,每次s[ i ] 和p[ j + 1 ]比较
当匹配过程到上图所示时,
s[ a , b ] = p[ 1, j ] && s[ i ] != p[ j + 1 ] 此时要移动p串(不是移动1格,而是直接移动到下次能匹配的位置)
其中1串为[ 1, next[ j ] ],3串为[ j - next[ j ] + 1 , j ]。由匹配可知 1串等于3串,3串等于2串。所以直接移动p串使1到3的位置即可。这个操作可由j = next[ j ]直接完成。 如此往复下去,当 j == m时匹配成功。
代码如下
for(int i = 1, j = 0; i <= n; i++){while(j && s[i] != p[j+1]) j = ne[j];//如果j有对应p串的元素, 且s[i] != p[j+1], 则失配, 移动p串//用while是由于移动后可能仍然失配,所以要继续移动直到匹配或整个p串移到后面(j = 0)
if(s[i] == p[j+1]) j++; //当前元素匹配,j移向p串下一位 if(j == m) { //匹配成功,进行相关操作 j = next[j]; //继续匹配下一个子串 }
}注:采用上述的匹配方法( i 与 j+1 比较)我不清楚(其实是想不清楚)为什么要这样。。。脑子有点不好使。而不推荐下标从0开始的原因我认为是:若下标从0开始的话,next[ ]数组的值都会相应-1,这就会导致它的实际含义与其定义的意思不符(部分匹配值和next数组值相差1),思维上有点违和,容易出错。(看了习题课,在实际操作上下标从0开始代码会多很多东西,比从1开始复杂一些,嗯。。。确实
四、求next数组的思路和实现代码 next数组的求法是通过模板串自己与自己进行匹配操作得出来的(代码和匹配操作几乎一样)。
代码如下
for(int i = 2, j = 0; i <= m; i++) { while(j && p[i] != p[j+1]) j = next[j]; if(p[i] == p[j+1]) j++; next[i] = j; }
代码和匹配操作的代码几乎一样,关键在于每次移动 i 前,将 i 前面已经匹配的长度记录到next数组中。
五、完整代码
// 注:这不是题目的AC代码,是一个最基本的模板代码 #include <iostream> using namespace std; const int N = 100010, M = 10010; //N为模式串长度,M匹配串长度 int n, m; int ne[M]; //next[]数组,避免和头文件next冲突 char s[N], p[M]; //s为模式串, p为匹配串 int main() { cin >> n >> s+1 >> m >> p+1; //下标从1开始 //求next[]数组 for(int i = 2, j = 0; i <= m; i++) { while(j && p[i] != p[j+1]) j = ne[j]; if(p[i] == p[j+1]) j++; ne[i] = j; } //匹配操作 for(int i = 1, j = 0; i <= n; i++) { while(j && s[i] != p[j+1]) j = ne[j]; if(s[i] == p[j+1]) j++; if(j == m) //满足匹配条件,打印开头下标, 从0开始 { //匹配完成后的具体操作 //如:输出以0开始的匹配子串的首字母下标 //printf("%d ", i - m); (若从1开始,加1) j = ne[j]; //再次继续匹配 } } return 0; }
哈希表
-
c++模拟散列表题目描述维护一个集合,支持如下几种操作:
“I x”,插入一个数x;“Q x”,询问数x是否在集合中出现过;现在要进行N次操作,对于每个询问操作输出对应的结果。
输入格式第一行包含整数N,表示操作数量。
接下来N行,每行包含一个操作指令,操作指令为”I x”,”Q x”中的一种。
输出格式对于每个询问指令“Q x”,输出一个询问结果,如果x在集合中出现过,则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1≤N≤105 −109≤x≤109
输入样例:
5 I 1 I 2 I 3 Q 2 Q 5
输出样例:
Yes No
拉链法
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N = 100003; //找比题设范围大且离2的整次幂最远的质数作为最大范围,是为了减少哈希映射的冲突 int n , h[N] , ne[N] , e[N] , idx; void insert(int x) { int k = (x % N + N) % N; //因为负数mod的特殊性-10 % 3 = 2,所以这样映射 e[idx] = x; //将x存储在idx结点(头插法插入) ne[idx] = h[k]; //存储x所在结点的指针(更改链表指向,建立链式结构) h[k] = idx ++; //h[k]储存指向新插入的数据指针(头指针指向新插入的数据)之后idx++ } //h[0] = -1 , ne[0] = h[0] = -1 , h[0] = idx = 0; //h[0] = 0 , ne[1] = h[0] = 0 , h[0] = idx = 1; bool find(int x) { int k = (x % N + N) % N; for(int i = h[k];i != -1;i = ne[i]) //h[k] 是头节点,ne[h[k]] 是头节点的前一结点 if(e[i] == x) return true; return false; } int main() { cin >> n; memset(h , -1 ,sizeof h); //初始化每个拉链处的头结点均为-1,模拟链表的头结点指向NULL(-1) char op[2]; while(n --) { int x; scanf("%s%d",op,&x); //巧妙地输入,避免输入的是上组数据的行末回车/空格 if(*op == 'I') insert(x); //插入操作 else if(*op == 'Q') //寻找操作 { if(find(x)) cout << "Yes"<<endl; else cout << "No"<< endl; } } return 0; }
开放寻址法思路开放寻址法采用hash函数找到在hash数组中对应的位置,如果该位置上有值,并且这个值不是寻址的值,则出现冲突碰撞,需要解决冲突方案,该算法采用简单的向右继续寻址来解决问题。
由于思想简单,将不深入探讨,在次,我们探讨一下文中常常让人摸不着头脑的参数
让人费解的参数
1. const int N = 200003; 1.1开放寻址操作过程中会出现冲突的情况,一般会开成两倍的空间,减少数据的冲突 1.2如果使用%来计算索引, 把哈希表的长度设计为素数(质数)可以大大减小哈希冲突 比如 10%8 = 2 10%7 = 3 20%8 = 4 20%7 = 6 30%8 = 6 30%7 = 2 40%8 = 0 40%7 = 5 50%8 = 2 50%7 = 1 60%8 = 4 60%7 = 4 70%8 = 6 70%7 = 0 这就是为什么要找第一个比空间大的质数
2.const int null = 0x3f3f3f3f 和 memset(h, 0x3f, sizeof h)之间的关系;
首先,必须要清楚memset函数到底是如何工作的 先考虑一个问题,为什么memset初始化比循环更快? 答案:memset更快,为什么?因为memset是直接对内存进行操作。memset是按字节(byte)进行复制的 void * memset(void *_Dst,int _Val,size_t _Size); 这是memset的函数声明 第一个参数为一个指针,即要进行初始化的首地址 第二个参数是初始化值,注意,并不是直接把这个值赋给一个数组单元(对int来说不是这样) 第三个参数是要初始化首地址后多少个字节 看到第二个参数和第三个参数,是不是又感觉了 h是int类型,其为个字节, 第二个参数0x3f八位为一个字节,所以0x3f * 4(从高到低复制4份) = 0x3f3f3f3f 这也说明了为什么在memset中不设置除了-1, 0以外常见的值 比如1, 字节表示为00000001,memset(h, 1, 4)则表示为0x01010101
3. 为什么要取0x3f3f3f,为什么不直接定义无穷大INF = 0x7fffffff,即32个1来初始化呢?
3.1 首先,0x3f3f3f的体验感很好,0x3f3f3f3f的十进制是1061109567,也就是10^9级别的 (和0x7fffffff一个数量级),而一般场合下的数据都是小于10^9的,所以它可以作为无穷大 使用而不致出现数据大于无穷大的情形。 比如0x3f3f3f3f+0x3f3f3f3f=2122219134,这非常大但却没有超过32-bit,int的表示范围, 所以0x3f3f3f3f还满足了我们“无穷大加无穷大还是无穷大”的需求。 但是INF不同,一旦加上某个值,很容易上溢,数值有可能转成负数,有兴趣的小伙伴可以去试一试。 3.2 0x3f3f3f3f还能给我们带来一个意想不到的额外好处:如果我们想要将某个数组清零, 我们通常会使用memset(a,0,sizeof(a))这样的代码来实现(方便而高效),但是当我们想 将某个数组全部赋值为无穷大时(例如解决图论问题时邻接矩阵的初始化),就不能使用 memset函数而得自己写循环了(写这些不重要的代码真的很痛苦),我们知道这是因为memset 是按字节操作的,它能够对数组清零是因为0的每个字节都是0, 现在如果我们将无穷大设为0x3f3f3f3f,那么奇迹就发生了,0x3f3f3f3f的每个字节都是0x3f!所以 要把一段内存全部置为无穷大,我们只需要memset(a,0x3f,sizeof(a))。
开放寻址法 #include<iostream> #include<cstring> using namespace std; const int N = 200003,null = 0x3f3f3f3f; int h[N]; /* memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值 s指向要填充的内存块。 c是要被设置的值。 n是要被设置该值的字符数。 返回类型是一个指向存储区s的指针。 */ int find(int x){ int k = (x%N+N)%N; while(h[k]!=null&&h[k]!=x){//当前的位置不为空,并且当前位置的值不为x。 k++;//去下一个位置 if(k == N)k =0;//走到头了,k = 0,重新回去再找一遍。 } return k;//返回找到x所在的下标。 } int main(){ int n; scanf("%d",&n); memset(h,0x3f,sizeof h);//将数组h初始化,每一个位置都设置为0x3f. while(n--){ char op[2];//用op[2]来写入字符,可以直接忽略回车和空格。 int x; scanf("%s%d",op,&x);//x为查询的点的值. int k =find(x);//k为要查询的点x的下标。 if(*op == 'I')h[find(x)] = x;//插入操作 else { if(h[k]!=null)puts("Yes");//要查询的点x存在。 else puts("No"); } } return 0; }
Trie字符串
-
Trie字符串统计维护一个字符串集合,支持两种操作:
I x 向集合中插入一个字符串 x;Q x 询问一个字符串在集合中出现了多少次。共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
输入格式第一行包含整数 N,表示操作数。
接下来 N 行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。
输出格式对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。
每个结果占一行。
数据范围1≤N≤2∗104输入样例:
5 I abc Q abc Q ab I ab Q ab
输出样例:
1 0 1
Trie树又称字典树、单词查找树。是一种能够高效存储和查找字符串集合的数据结构。
用数组来模拟Trie树
插入操作代码:
void insert(char *str) { int p = 0; //类似指针,指向当前节点 for(int i = 0; str[i]; i++) { int u = str[i] - 'a'; //将字母转化为数字 if(!son[p][u]) son[p][u] = ++idx; //该节点不存在,创建节点,其值为下一个节点位置 p = son[p][u]; //使“p指针”指向下一个节点位置 } cnt[p]++; //结束时的标记,也是记录以此节点结束的字符串个数 }
查找操作代码:
int query(char *str) { int p = 0; for(int i = 0; str[i]; i++) { int u = str[i] - 'a'; if(!son[p][u]) return 0; //该节点不存在,即该字符串不存在 p = son[p][u]; } return cnt[p]; //返回字符串出现的次数 }
完整代码
//Trie树快速存储字符集合和快速查询字符集合 #include <iostream> using namespace std; const int N = 100010; //son[][]存储子节点的位置,分支最多26条; //cnt[]存储以某节点结尾的字符串个数(同时也起标记作用) //idx表示当前要插入的节点是第几个,每创建一个节点值+1 int son[N][26], cnt[N], idx; char str[N]; void insert(char *str) { int p = 0; //类似指针,指向当前节点 for(int i = 0; str[i]; i++) { int u = str[i] - 'a'; //将字母转化为数字 if(!son[p][u]) son[p][u] = ++idx; //该节点不存在,创建节点 p = son[p][u]; //使“p指针”指向下一个节点 } cnt[p]++; //结束时的标记,也是记录以此节点结束的字符串个数 } int query(char *str) { int p = 0; for(int i = 0; str[i]; i++) { int u = str[i] - 'a'; if(!son[p][u]) return 0; //该节点不存在,即该字符串不存在 p = son[p][u]; } return cnt[p]; //返回字符串出现的次数 } int main() { int m; cin >> m; while(m--) { char op[2]; scanf("%s%s", op, str); if(*op == 'I') insert(str); else printf("%d\n", query(str)); } return 0; }
堆排序
838.堆排序输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
输出格式共一行,包含 m 个整数,表示整数数列中前 m 小的数。
数据范围1≤m≤n≤105,1≤数列中元素≤109输入样例:
5 3 4 5 1 3 2
输出样例:
1 2 3
如何手写一个堆?完全二叉树 5个操作
-
插入一个数 heap[ ++ size] = x; up(size);
-
求集合中的最小值 heap[1]
-
删除最小值 heap[1] = heap[size]; size -- ;down(1);
-
删除任意一个元素 heap[k] = heap[size]; size -- ;up(k); down(k);
-
修改任意一个元素 heap[k] = x; up(k); down(k);
#include<iostream> using namespace std; const int N = 100010; int n,m; int h[N],cnt; //h[i] 表示第i个结点存储的值,i从1开始,2*i是左子节点,2*i + 1是右子节点 //cnt 既表示堆里存储的元素个数,又表示最后一个结点的下标 void down(int u){ int t = u;//t存储三个点中最小的节点的下标,初始化为当前节点u。 if(u*2<=siz && h[u*2]<h[t])t = u*2;//当前节点的左儿子存在 并且 当前节点的左儿子的值小于当前节点。更新t的值为左儿子的下标 if(u*2+1<=siz && h[u*2+1]<h[t])t=u*2+1;//当前节点的右儿子存在 并且 当前节点的右儿子的值小于当前节点。更新t的值为右儿子的下标 if(u!=t){//当经过上面两个判断后 当前节点的下标u不为最小的节点下标,如果t==u意味着不用变动,u就是三个节点中拥有最小值的节点下标,否则交换数值。 swap(h[u],h[t]); down(t);//交换数值后,t这个结点存储原本u的值,u存储存储t的值(三个数中的最小值)。 //u不用调整了,但t情况不明,可能需要调整。直到它比左右子节点都小 } } int main(){ scanf("%d%d",&n,&m); for(int i = 1;i<=n;i++)scanf("%d",&h[i]); cnt = n;//初始化size,表示堆里有n 个元素 for(int i = n/2 ; i ; i --)down(i);//把堆初始化成小根堆,从二叉树的倒数第二行开始,把数字大的下沉 //我认为从n/2开始,还有一个角度可以理解, //因为n是最大值,n/2是n的父节点, //因为n是最大,所以n/2是 最大的有子节点的父节点, //所以从n/2往前遍历,就可以把整个数组遍历一遍. while( m-- ){ printf("%d ",h[1]);//删除最小值 h[1] = h[siz];//把最下面的点覆盖第一个点。 siz--;//堆里点的个数减一。 down(1);//此时第一个点为堆里的最后一个点(第一个点被覆盖了),把这个点再重新排序(往下走)。 } return 0; }
搜索与图论
dfs
如何用 dfs 解决全排列问题?
dfs 最重要的是搜索顺序。用什么顺序遍历所有方案。
对于全排列问题,以 n = 3 为例,可以这样进行搜索:
假设有 3 个空位,从前往后填数字,每次填一个位置,填的数字不能和前面一样。
最开始的时候,三个空位都是空的:__ __ __
首先填写第一个空位,第一个空位可以填 1,填写后为:1 __ __
填好第一个空位,填第二个空位,第二个空位可以填 2,填写后为:1 2 __
填好第二个空位,填第三个空位,第三个空位可以填 3,填写后为: 1 2 3
这时候,空位填完,无法继续填数,所以这是一种方案,输出。
然后往后退一步,退到了状态:1 2 __ 。剩余第三个空位没有填数。第三个空位上除了填过的 3 ,没有其他数字可以填。
因此再往后退一步,退到了状态:1 __ __。第二个空位上除了填过的 2,还可以填 3。第二个空位上填写 3,填写后为:1 3 __
填好第二个空位,填第三个空位,第三个空位可以填 2,填写后为: 1 3 2
这时候,空位填完,无法继续填数,所以这是一种方案,输出。
然后往后退一步,退到了状态:1 3 __ 。剩余第三个空位没有填数。第三个空位上除了填过的 2,没有其他数字可以填。
因此再往后退一步,退到了状态:1 __ __。第二个空位上除了填过的 2,3,没有其他数字可以填。
因此再往后退一步,退到了状态:__ __ __。第一个空位上除了填过的 1,还可以填 2。第一个空位上填写 2,填写后为:2 __ __
填好第一个空位,填第二个空位,第二个空位可以填 1,填写后为:2 1 __
填好第二个空位,填第三个空位,第三个空位可以填 3,填写后为:2 1 3
这时候,空位填完,无法继续填数,所以这是一种方案,输出。
然后往后退一步,退到了状态:2 1 __ 。剩余第三个空位没有填数。第三个空位上除了填过的 3,没有其他数字可以填。
因此再往后退一步,退到了状态:2 __ __。第二个空位上除了填过的 1,还可以填 3。第二个空位上填写 3,填写后为:2 3 __
填好第二个空位,填第三个空位,第三个空位可以填 1,填写后为:2 3 1
这时候,空位填完,无法继续填数,所以这是一种方案,输出。
然后往后退一步,退到了状态:2 3 __ 。剩余第三个空位没有填数。第三个空位上除了填过的 1,没有其他数字可以填。
因此再往后退一步,退到了状态:2 __ __。第二个空位上除了填过的 1,3,没有其他数字可以填。
因此再往后退一步,退到了状态:__ __ __。第一个空位上除了填过的 1,2,还可以填 3。第一个空位上填写 3,填写后为:3 __ __
填好第一个空位,填第二个空位,第二个空位可以填 1,填写后为:3 1 __
填好第二个空位,填第三个空位,第三个空位可以填 2,填写后为:3 1 2
这时候,空位填完,无法继续填数,所以这是一种方案,输出。
然后往后退一步,退到了状态:3 1 __ 。剩余第三个空位没有填数。第三个空位上除了填过的 2,没有其他数字可以填。
因此再往后退一步,退到了状态:3 __ __。第二个空位上除了填过的 1,还可以填 2。第二个空位上填写 2,填写后为:3 2 __
填好第二个空位,填第三个空位,第三个空位可以填 1,填写后为:3 2 1
这时候,空位填完,无法继续填数,所以这是一种方案,输出。
然后往后退一步,退到了状态:3 2 __ 。剩余第三个空位没有填数。第三个空位上除了填过的 1,2,没有其他数字可以填。
因此再往后退一步,退到了状态:3 __ __。第二个空位上除了填过的 1,2,没有其他数字可以填。
因此再往后退一步,退到了状态:__ __ __。第一个空位上除了填过的 1,2,3,没有其他数字可以填。
此时深度优先搜索结束,输出了所有的方案。
算法:
-
用 path 数组保存排列,当排列的长度为 n 时,是一种方案,输出。
-
用 state 数组表示数字是否用过。当 state[i] 为 1 时:i 已经被用过,state[i] 为 0 时,i 没有被用过。
-
dfs(i) 表示的含义是:在 path[i] 处填写数字,然后递归的在下一个位置填写数字。
-
回溯:第 i 个位置填写某个数字的所有情况都遍历后, 第 i 个位置填写下一个数字。代码
#include<iostream> using namespace std; const int N = 10; int path[N];//保存序列 int stats[N];//数字是否被用过 int n; void dfs(int u){//深度优先搜索 u为当前用到的位置 if(u > n){ //n个位置填完了 for(int i = 1;i<=n;i++){ cout<<path[i]<<" "; //输出这个序列 } cout<<endl; } for(int i = 1;i<=n;i++){ //每一个空位上可以选择填写的数字大小为:1~n if(!stats[i]){ //数字i没有被用过 path[u] = i; //把数字i放到u这个空位上 stats[i] = 1; //数字i现在被用了 dfs(u+1); //递归填下一个位置 stats[i] = 0; //回溯(恢复现场) } } } int main(){ cin>>n; dfs(1); return 0; }
中等
相关标签
相关企业
提示
沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。
由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋 。
小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。
给你一个整数数组 nums
表示每间房屋存放的现金金额。形式上,从左起第 i
间房屋中放有 nums[i]
美元。
另给你一个整数 k
,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少 k
间房屋。
返回小偷的 最小 窃取能力。
示例 1:
输入:nums = [2,3,5,9], k = 2 输出:5 解释: 小偷窃取至少 2 间房屋,共有 3 种方式: - 窃取下标 0 和 2 处的房屋,窃取能力为 max(nums[0], nums[2]) = 5 。 - 窃取下标 0 和 3 处的房屋,窃取能力为 max(nums[0], nums[3]) = 9 。 - 窃取下标 1 和 3 处的房屋,窃取能力为 max(nums[1], nums[3]) = 9 。 因此,返回 min(5, 9, 9) = 5 。
示例 2:
输入:nums = [2,7,9,3,1], k = 2 输出:2 解释:共有 7 种窃取方式。窃取能力最小的情况所对应的方式是窃取下标 0 和 4 处的房屋。返回 max(nums[0], nums[4]) = 2 。
class Solution { public int minCapability(int[] nums, int k) { int lower = Arrays.stream(nums).min().getAsInt(); // 找到最小的能力下界 int upper = Arrays.stream(nums).max().getAsInt(); // 找到最大的能力上界 while (lower <= upper) { int middle = (lower + upper) / 2; // 计算中间值 int count = 0; // 用于计算分配任务的工人数量 boolean visited = false; // 用于确保每个工人只分配一次任务 for (int x : nums) { if (x <= middle && !visited) { count++; // 如果当前任务的能力要求不超过中间值,分配给一个工人 visited = true; // 标记工人已分配任务 } else { visited = false; // 如果任务的能力要求超过了中间值,不分配任务,重置 visited } } if (count >= k) { upper = middle - 1; // 如果分配的工人数量满足或超过 k,减小上界 } else { lower = middle + 1; // 如果分配的工人数量不足 k,增加下界 } } return lower; // 返回最小的能力下界,这个能力可以满足分配 k 个任务的要求 } }
这段代码使用二分查找来找到满足分配 k
个任务的最小能力要求。以下是它的详细步骤:
-
首先,它找到了任务的能力要求中的最小值
lower
和最大值upper
。这些值将用于定义搜索范围。 -
接下来,它进入了一个二分查找的循环,直到
lower
大于upper
才结束。 -
在每一次迭代中,它计算中间值
middle
。 -
然后,它模拟了分配任务的过程。对于每个任务的能力要求
x
,如果x
不超过当前中间值middle
,并且之前的任务没有分配给工人(通过visited
来判断),则将任务分配给一个工人,同时标记visited
为true
。 -
如果分配的工人数量
count
大于等于k
,表示当前中间值middle
可以满足要求,因此将upper
缩小为middle - 1
继续搜索更小的能力。 -
如果分配的工人数量
count
小于k
,表示当前中间值middle
不足以满足要求,因此将lower
增加为middle + 1
继续搜索更大的能力。 -
最终,当
lower
超过了upper
时,二分查找结束,返回lower
作为满足分配k
个任务要求的最小能力。
这段代码的核心思想是利用二分查找来逼近最小的能力要求,以满足分配 k
个任务的要求。
Floyd求最短路
5033. 最远距离 给定一个 n 个点的无向完全图,点的编号 1∼n 。 图中所有边的长度已知。 我们规定,两点之间的距离指两点之间的最短路径长度。 请你计算,给定图中距离最远的两个点之间的距离。 输入格式 第一行包含整数 n 。 接下来 n 行,每行包含 n 个整数,其中第 i 行第 j 个整数 aij 表示连接点 i 和点 j 的边的长度。 输出格式 一个整数,表示给定图中距离最远的两个点之间的距离。 数据范围 前 3 个测试点满足 3≤n≤4 。 所有测试点满足 3≤n≤10 ,1≤aij≤100 ,aij=aji ,aii=0 。 输入样例1: 3 0 1 1 1 0 4 1 4 0 输出样例1: 2 输入样例2: 4 0 1 2 3 1 0 4 5 2 4 0 6 3 5 6 0 输出样例2: 5
#include<bits/stdc++.h> using namespace std; const int N = 110; const int MAXX = 0x3f3f3f3f; int a[N][N]; int main(){ int n; cin>>n; for(int i = 1;i<=n;i++){ for(int j = 1;j<=n;j++){ cin>>a[i][j]; } } // Floyd 算法求解最短路径 for (int k = 1; k <= n; k ++) for (int i = 1; i <= n; i ++) for (int j = 1; j <= n; j ++) a[i][j] = min(a[i][j], a[i][k] + a[k][j]); int res = 0; for(int i = 1;i<=n;i++){ for(int j = 1;j<=n;j++){ if(a[i][j]!=MAXX){ res = max(res,a[i][j]); } } } cout<<res<<endl; return 0; }
解释一从任意顶点 i 到任意顶点 j 的最短路径不外乎两种可能① 直接从 i 到 j② 从 i 经过若干个顶点到 j假设 dist(i,j) 为顶点 i 到顶点 j 的最短路径的距离对于每一个顶点 k,检查 dist(i,k) + dist(k,j)<dist(i,j) 是否成立如果成立,证明从 i 到 k 再到 j 的路径比 i 直接到 j 的路径短,设置 dist(i,j) = dist(i,k) + dist(k,j)当我们遍历完所有结点 k,dist(i,j) 中记录的便是 i 到 j 的最短路径的距离
解释二解题思路,动态规划的思想
该解题思路抄自 AcWing 854. Floyd求最短路---yxc---java解法,附详细分析 - AcWing假设节点序号是从1到n。假设f0[j]是一个n*n的矩阵,第i行第j列代表从i到j的权值,如果i到j有边,那么其值就为ci,j(边ij的权值)。如果没有边,那么其值就为无穷大。fk[j]代表(k的取值范围是从1到n),在考虑了从1到k的节点作为中间经过的节点时,从i到j的最短路径的长度。比如,f1[j]就代表了,在考虑了1节点作为中间经过的节点时,从i到j的最短路径的长度。分析可知,f1[j]的值无非就是两种情况,而现在需要分析的路径也无非两种情况,i=>j,i=>1=>j:【1】f0[j]:i=>j这种路径的长度,小于,i=>1=>j这种路径的长度【2】f0[1]+f0[j]:i=>1=>j这种路径的长度,小于,i=>j这种路径的长度形式化说明如下:fk[j]可以从两种情况转移而来:【1】从fk−1[j]转移而来,表示i到j的最短路径不经过k这个节点【2】从fk−1[k]+fk−1[j]转移而来,表示i到j的最短路径经过k这个节点
总结就是:fk[j]=min(fk−1[j],fk−1[k]+fk−1[j])从总结上来看,发现f[k]只可能与f[k−1]有关。