常用数据结构
单调队列
求出连续区间内的最大值或最小值 ,维护一个单调的队列。
`
/*
ans数组维护第k个区间最大最小值。a数组存储数据,q数组模拟队列,m是区间长度,h维护队头指针
*/
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
h=1;t=0;
for(int i=1;i<=n;i++){//以i为结尾
while(h<=t && a[q[t]]<=a[i])t--;//删去队尾的无用元素 ,修改这里改变最大最小
q[++t]=i;//进队
while(i-q[h]>=m)h++;//队头删去在所需区间外的元素
if(i>=m)ans[i-m+1]= a[q[h]];//这么写省了先入m个元素的问题,便捷
}
`
单调栈
很方便地求出某个数的左边或者右边第一个比它大或者小的元素,而且总时间复杂度O(N)。
//求出右边第一个比他大的元素下标
//a存数据,q存栈中下标,t维护栈顶。ans维护每个点右边第一个比他大的元素的下标,整体为单调递减栈
int t = 0;
for(int i = n; i>=1; i -- )//待修改
{
while(t && a[q[t - 1]] <= a[i]) t --;//待修改
if(t) ans[i] = q[t - 1];
q[t ++ ] = i;
}
跳跃表
字符串
KMP
定义:有一个文本串S,和一个模式串P,现在要查找P在S中的位置
方法:①建立next数组(即最大公共前缀后缀),使得模式串可以更方便的进行回溯,不用从头在开始匹配。
② 通过while循环获取到第一个位置。
③ 周期性循环节长度为 : len-next【len】
#include<bits/stdc++.h>
using namespace std ;
string s,p;
const int maxn=1e5;
int nxtpos0[maxn];//nxtpos0为未优化版nxtpos数组
void getnxt()//获取next数组
{
nxtpos0[0]=-1;
int len = p.size();
int j=0,k=-1;
while(j <= len - 1)
{
if(k==-1||p[j]==p[k])
{
j++;
k++;
nxtpos0[j]=k;
}
else
{
k = nxtpos0[k];
}
}
}
void kmp()//对模式串和文本串进行匹配
{
int i=0,j=0;
int slen=s.size();
int plen=p.size();
while(i < slen)
{
if(j == -1||p[j] == s[i])
{
i++;
j++;
}
else
{
j=nxtpos0[j];
}
if(j==plen)
{
cout<<i-j+1<<"\n";//输出匹配的到的模式串位置。
i=i-j+1;
j=0;
}
}
}
int main()
{
cin>>s>>p;
getnxt();
kmp();
return 0;
}
exKMP
定义母串S,和字串T,extend[i]表示T与S[i,n-1]的最长公共前缀。
https://blog.youkuaiyun.com/yiqzq/article/details/98663132?utm_medium=distribute.pc_relevant_download.none-task-blog-baidujs-1.nonecase&depth_1-utm_source=distribute.pc_relevant_download.none-task-blog-baidujs-1.nonecase
Manacher算法
字典树
也可解决区间亦或最值问题
//指针型字典树
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
struct trie//trie型结构体,建立树
{
int cnt;//用作标记查找
trie *next[26];//连接树与节点的指针
bool flag;///单词结尾节点标记
};
trie *root=new trie;//建立根节点
void insert(char ch[])
{
trie *p=root,*newnode;
for(int i=0; ch[i]!='\0'; i++)
{
if(p->next[ch[i]-'a']==0)//这里以所有的字符全部为小写字母为例
{
newnode=new trie;///创建新节点
newnode->flag=false;///初始化新节点结尾标记
for(int j=0; j!=26; j++)
{
newnode->next[j]=NULL;///初始化所有孩子
}
newnode->cnt=1;///新节点次数初始化
p->next[ch[i]-'a']=newnode;
p=newnode;
}
else
{
p=p->next[ch[i]-'a'];
p->cnt++;///对走过的节点次数进行计数
}
}
p->flag=true;///标记结尾
}
int find(char ch[])///查询
{
trie *p=root;
for(int i=0; ch[i]!='\0'; i++)
{
if(p->next[ch[i]-'a']!=NULL)
p=p->next[ch[i]-'a'];
else
return 0;
}
return p->cnt;
}
int deal(trie* T)//递归将字典树清空
{
int i;
if(T==NULL)
return 0;
for(i=0;i<10;i++)
{
if(T->next[i]!=NULL)
deal(T->next[i]);
}
free(T);
return 0;
}
int main()
{
char ch[20];
for(int i=0; i!=26; i++)
{
root->next[i]=NULL;
}
root->cnt=0;
while(gets(ch))
{
if(!strcmp(ch,""))
break;
insert(ch);
}
while(scanf("%s",ch)!=EOF)
{
printf("%d\n",find(ch));
}
deal(root);
return 0;
}
//数组型字典树
//
#include <stdio.h>
#include <string.h>
const int maxn=10000;//提前估计好可能会开的节点的个数
int tot; //节点编号,模拟申请新节点,静态申请
int trie[10000][26]; //假设每个节点的分支有26个
bool isw[10000]; //判断该节点是不是单词结尾
void insert(char *s,int rt)//投入的参数是一个字符串和节点数,建立字典树
{
for(int i=0; s[i]; i++)
{
int x=s[i]-'a';//假设单词都是小写字母组成
if(trie[rt][x]==0) //没有,申请新节点
{
trie[rt][x]=++tot;//每个字符的编号
}
rt=trie[rt][x];
}
isw[rt]=true;//整个字符串读完后,在isw数组中记录第rt层为单词结尾
}
bool find(char *s,int rt)
{
for(int i=0; s[i]; i++)
{
int x=s[i]-'a';//假设单词都是小写字母组成
if(trie[rt][x]==0)
{
return false;
}
rt=trie[rt][x];
}
return isw[rt];
}
char s[22];//单词读入
int main()
{
tot=0;//一开始没有节点
int rt=++tot;//申请一个根节点
memset(trie[rt],0,sizeof(trie[rt]));//初始化根节点
memset(isw,false,sizeof(isw));
while(scanf("%s",s),s[0]!='#') //新建字典,以一个'#'结束
{
insert(s,rt);
}
while(scanf("%s",s),s[0]!='#') //查单词,以一个'#'结束
{
if(find(s,rt))//从字典中查找单词
printf("%s 在字典中\n",s);
else
printf("%s 不在字典中\n",s);
}
return 0;
}
AC自动机
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Node
{
int cnt;//是否为该单词的最后一个结点
Node *fail;//失败指针
Node *next[26];//Trie中每个结点的各个节点
}*queue[500005];//队列,方便用BFS构造失败指针
char s[1000005];//主字符串
char keyword[55];//需要查找的单词
Node *root;//头结点
void Init(Node *root)//每个结点的初始化
{
root->cnt=0;
root->fail=NULL;
for(int i=0;i<26;i++)
root->next[i]=NULL;
}
void Build_trie(char *keyword)//构建Trie树
{
Node *p,*q;
int i,v;
int len=strlen(keyword);
for(i=0,p=root;i<len;i++)
{
v=keyword[i]-'a';
if(p->next[v]==NULL)
{
q=(struct Node *)malloc(sizeof(Node));
Init(q);
p->next[v]=q;//结点链接
}
p=p->next[v];//指针移动到下一个结点
}
p->cnt++;//单词最后一个结点cnt++,代表一个单词
}
void Build_AC_automation(Node *root)
{
int head=0,tail=0;//队列头、尾指针
queue[head++]=root;//先将root入队
while(head!=tail)
{
Node *p=NULL;
Node *temp=queue[tail++];//弹出队头结点
for(int i=0;i<26;i++)
{
if(temp->next[i]!=NULL)//找到实际存在的字符结点
{ //temp->next[i] 为该结点,temp为其父结点
if(temp==root)//若是第一层中的字符结点,则把该结点的失败指针指向root
temp->next[i]->fail=root;
else
{
//依次回溯该节点的父节点的失败指针直到某节点的next[i]与该节点相同,
//则把该节点的失败指针指向该next[i]节点;
//若回溯到 root 都没有找到,则该节点的失败指针指向 root
p=temp->fail;//将该结点的父结点的失败指针给p
while(p!=NULL)
{
if(p->next[i]!=NULL)
{
temp->next[i]->fail=p->next[i];
break;
}
p=p->fail;
}
//让该结点的失败指针也指向root
if(p==NULL)
temp->next[i]->fail=root;
}
queue[head++]=temp->next[i];//每处理一个结点,都让该结点的所有孩子依次入队
}
}
}
}
int query(Node *root)
{ //i为主串指针,p为模式串指针
int i,v,count=0;
Node *p=root;
int len=strlen(s);
for(i=0;i<len;i++)
{
v=s[i]-'a';
//由失败指针回溯查找,判断s[i]是否存在于Trie树中
while(p->next[v]==NULL && p!=root)
p=p->fail;
p=p->next[v];//找到后p指针指向该结点
if(p==NULL)//若指针返回为空,则没有找到与之匹配的字符
p=root;
Node *temp=p;//匹配该结点后,沿其失败指针回溯,判断其它结点是否匹配
while(temp!=root)//匹配结束控制
{
if(temp->cnt>=0)//判断该结点是否被访问
{
count+=temp->cnt;//由于cnt初始化为 0,所以只有cnt>0时才统计了单词的个数
temp->cnt=-1;//标记已访问过
}
else//结点已访问,退出循环
break;
temp=temp->fail;//回溯 失败指针 继续寻找下一个满足条件的结点
}
}
return count;
}
int main()
{
int T,n;
scanf("%d",&T);
while(T--)
{
root=(struct Node *)malloc(sizeof(Node));
Init(root);
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("\n%s",keyword);
Build_trie(keyword);
}
Build_AC_automation(root);
scanf("\n%s",s);
printf("%d\n",query(root));
}
return 0;
}
字符串hash
字符串hash就是通过不同的进制,将字符串变成数字来做hash,通过前缀的方式,可以O(1)快速查询某个子串是否出现过,同时可以水过数据范围小的一些字符串问题。
//核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
//小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N=1e6+5;
ULL h[N], power[N]; // h[k]存储字符串前k个字母的哈希值, power[k]存储 P^k mod 2^64
char str[N];
int len,P;
// 初始化
void calHash()
{
P=131;
power[0] = 1;
for (int i = 1; i <= len; i ++ )
{
h[i] = h[i - 1] * P + (str[i]-'a'+1);
power[i] = power[i - 1] * P;
}
}
// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * power[r - l + 1];
}
//该题是判断一个字符串两个区间的字符串是否相同
int main()
{
scanf("%s",str+1);
len=strlen(str+1);
calHash();
int m;
scanf("%d",&m);
while(m--)
{
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if(get(l1,r1)==get(l2,r2))
{
printf("Yes\n");
}
else
{
printf("No\n");
}
}
return 0;
}
图论
最小生成树
Prim算法(适用点少)
Kruskal算法(适用边少)
最优比率生成树
最小k度限制生成树
Tarjan算法(缩点,算环)
矩阵快速幂(可求点到点的经历k条可重边的方案)
struct Mat{
long long mat[111][111];
Mat(){
memset(mat,0,sizeof(mat));
}
};
Mat operator*(const Mat &m1,const Mat &m2){
Mat m;
for(int i=0; i<=tn; ++i){
for(int j=0; j<=tn; ++j){
for(int k=0; k<=tn; ++k){
m.mat[i][j]+=m1.mat[i][k]*m2.mat[k][j];
m.mat[i][j]%=100000;
}
}
}
return m;
}
Mat e,x;
for(int i=0; i<=tn; ++i) e.mat[i][i]=1;
for(int i=0; i<=tn; ++i){//对矩阵进行标记。
if(flag[i]) continue;
for(int j=0; j<4; ++j){
if(flag[ch[i][j]]) continue;
++x.mat[i][ch[i][j]];
}
}
while(n){
if(n&1) e=e*x;
x=x*x;
n>>=1;
}
DFS
发现数据范围过大时,考虑双向dfs,即可将2的n此方复杂度变成2的n/2次方
BFS
在做bfs时,应保证队列中的元素具有单调性,即应为一层一层向外扩展。
如果出现单层bfs或dfs过不去的情况,考虑双向bfs,即从起点和终点向中间扩展,时间为指数级缩小,
搜索
迭代加深:
A*:
IDA*:
拓扑排序(判断图是否为DAG图)
判断一个图是否是 有向无环图(DAG) 。 拓扑排序原理: 对 DAG 的顶点进行排序,使得对每一条有向边 (u, v),均有 u(在排序记录中)比 v 先出现。亦可理解为对某点 v 而言,只有当 v 的所有源点均出现了,v 才能出现。
BFS法进行论证:
算法流程:
1.首先计算出所有节点的入度表。设nums为节点的数量和
2.使用队列进行广度优先搜索,将所有入度为0的点入队,然后一边出队,一边判断哪些点的入度变为0了,同时将他们入队,不断迭代。
3.每次执行出队操作时,将nums–,若存在如1-0,0-1的自环,则nums永不会为0,此时证明有环存在。
数论
叉乘,判断从同一个点出发的两直线相对顺时针or逆时针位置
double ans=(mx1-mid_x)*(my2-mid_y)-(mx2-mid_x)*(my1-mid_y);
ans>0||ans<0||ans=0;
矩阵快速幂
约数个数和欧拉函数
1.约数个数,存在N=p1的l1次方乘p2的l2次方*…乘Pk的lk次方
约数个数即为(l1+1)乘(l2+1)乘(l3+1)
2.1到N中,从i=1加到i=n f(i) = nlog(n);
3.int范围内,最多的因字数有1600个。
阶乘分解:例如将5!分解为2的3次方乘3乘5;
分解方式:
for(long long i=2;i<=n;i++)
if (!vis[i])
{
long long ans=0;
cout<<i<<" ";
for(long long k=1;power(i,k)<=n;k++)//统计p出现的次数
ans=ans+n/power(i,k);
cout<<ans<<endl;
}
杂论
ST表(区间最大最小值)
预:nlogn,查询为O(1)
//查询范围0-n-1
int a[1010];//原始输入数组
int st[1010][20];//st表
void init(int n)
{
for (int i = 0; i < n; i++)
st[i][0] = a[i];
for (int i = 1; (1 << i) <= n; i++){
for (int j = 0; j + (1 << i) - 1 < n; j++)
st[j][i] = min(st[j][i - 1],st[j + (1 << (i - 1))][i - 1]);
}
}
int search(int l, int r)
{
int k = (int)(log((double)(r - l + 1)) / log(2.0));
return min(st[l][k],st[r - (1 << k) + 1][k]);
}
博弈论
巴什博弈
即为一堆物品两个人轮流去,一次取一个或至多m个,问后手胜利前提下每次至多取多少个。
简称为:给你一个k,求最小的L使得k%(1+L)=0;
L为可以使第二个玩家获胜每次取纽扣的最大数量。
简单为:寻找k的大于等于2的因子,若能找到,则可确定第二个玩家必胜下每次最多选取的数量。
k倍动态减法有序(红书481页):
例如,也是一堆物品,轮流取,后一个人至多取前一个人的k倍,问谁赢
威佐夫博弈
即为两堆物品两个人轮流取,一次在一堆取k个或者两堆各取k个,问后手胜利前提下每次至多取多少个。
面对非奇异局势,先拿者必胜,反之,后拿者必胜。
通过floor((b-a)*((1.0+sqrt(5.0)/2.0)==a判断奇异局势。
尼姆博奕
简要解法:所有数取亦或判断是否为0,再或者取余再取亦或,再或者奇数堆取亦或或偶数堆取亦或。
n堆物品,轮流取
判断方式:将所有数亦或起来,判断最后的数是否为0,来判断先后手胜。
若询问第一步可创造出多少种必败态:
则先算出最后亦或结果t,判断有多少个数t^a[i]<a[i];
每堆至多取m个,此时将所有堆对m取余,在进行亦或运算判断0的情况
动态规划
待补:BZOJ5424
取数问题
例如:给定一个矩阵,从左上角向右下角取数,取两轮,第一轮被取过得数会变成0,问两轮后取得最大值。
解法:建立二维数组f【k】【i1】【i2】,k表示当前一共走了多少步,i1为目前第一轮的位置,i2为第二轮的位置,所以k-i1和k-i2确定具体坐标,代码如下:
for(int k = 2; k <= 2*n; k++) {
for(int i1 = 1; i1 <= k-1; i1++) {
for(int i2 = 1; i2 <= k-1; i2++) {
int j1 = k-i1, j2 = k-i2;
int &x = f[k][i1][i2];
int t = w[i1][j1];
if(i1!=i2) t += w[i2][j2];
//不重合则需要加两个权重.
x = max(x, f[k-1][i1-1][i2-1]+t);
x = max(x, f[k-1][i1-1][i2]+t);
x = max(x, f[k-1][i1][i2-1]+t);
x = max(x, f[k-1][i1][i2]+t);
//保留最大属性
}
}
}
cout << f[n*2][n][n] << endl;//输出时注意后两个属性应相同,为行数。
return 0;
01背包
STL用法
unordered_map(哈希表)
//unordered_map内存占用率较高
unordered_map<int, string> myMap={{ 5, "张大" },{ 6, "李五" }};//使用{}赋值
myMap[2] = "李四"; //使用[ ]进行单个插入,若已存在键值2,则赋值修改,若无则插入。
myMap.insert(pair<int, string>(3, "陈二"));//使用insert和pair插入,直接赋值也可以
优先队列
//升序队列,小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//降序队列,大顶堆
priority_queue <int,vector<int>,less<int> >q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
运算符重载
struct node //运算符重载<
{
int x;
friend bool operator< (node n1, node n2)
{
return n1.x < n2.x;
}
};
问题总结:
图论算边权时
1.图论算顶点到叶子节点的路径值的和时,如果有除以2的操作,一定注意是仅针对某一条边,还是整条路径。尤其是向下取整时,构造奇数边权的样例防止错误。