模板题
洛谷-PJ组
如何更好地食用 UVa OJ - 洛谷专栏
高精计算
字典树求解A+B
以每个数字建立字典树,建立一次查询一次
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
using namespace std;
struct node{
int str[26];
int sum;
}s[1000];
char str1[100];
int t=0,tot=0,ss=0;
bool f1;
void built()
{
t=0;
for(int i=0;i<strlen(str1);i++)
{
if(str1[i]=='-'){
f1=true;continue;
}
if(!s[t].str[str1[i]-'0'])
s[t].str[str1[i]-'0']=++tot;
t=s[t].str[str1[i]-'0'];
s[t].sum=str1[i]-'0';
}
}
int query()
{
int t=0;int s1=0;
for(int i=0;i<strlen(str1);i++)
{
if(str1[i]=='-') continue;
if(!s[t].str[str1[i]-'0']) return s1;
t=s[t].str[str1[i]-'0'];
s1=s1*10+s[t].sum;
}
return s1;
}
int main()
{
for(int i=1;i<=2;i++)
{
f1=false;
scanf("%s",str1);
built();
if(f1)
ss-=query();
else ss+=query();
}
printf("%d",ss);
return 0;
}
A+B
竖式加减
ps:我抄我自己19年的题解抄的理直气壮.jpg
#include <string>
#include <iostream>
using namespace std;
int a[100],b[100],c[100];
int i,lena,lenb,lenc=1;
int main(){
string a1,b1;
cin >> a1 >> b1;
lena = a1.length();
lenb = b1.length();
for(i=1;i<=lena;i++) a[i] = a1[lena-i]-48;
for(i=1;i<=lenb;i++) b[i] = b1[lenb-i]-48;
int x = 0;
while(lenc<=lena || lenc<=lenb){
c[lenc] = a[lenc]+b[lenc]+x;
x = c[lenc]/10;
c[lenc]%=10;
lenc++;
}
c[lenc] = x;
if(c[lenc]==0) lenc--;
for(i=lenc;i>0;i--) cout<<c[i];
return 0;
}
A*B
基础数据结构
常用板子
https://www.zhihu.com/question/288096930
//大小比较
#define max(a,b) a>b?a:b
#define min(a,b) a<b?a:b
//快读
inline int read(){
int f=1,x=0;char ch;
do{ch=getchar();if(ch==' ')f=-1;}while(ch<'0'||ch>'9');
do{x=x*10+ch-'0';ch=getchar();}while(ch>='0'&&ch<='9');
return f*x;
}
//快输
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
//万能头 + 解流同步
#include <bits/stdc++.h>
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
// 手动long long long 两个long long(9.8*1e18)当高精度用
const ll t=(ll)le9;
struct pii{
ll fi,se;
pii(){fi=0,se=0;} //se高位 fi低位
pii(ll b){fi=b%t; se=b/t;}
friend pii operator +(pii a, pii b){
pii c;
c.fi=a.fi+b.fi;
c.se=a.se+b.se;
if(c.fi<0 and c.se>0) c.se--,c.fi++;
if(c.fi>t and c.se>=0) c.se+=c.fi/t,c.fi%=t;
if(c.fi>0 and c.se<0) c.se++,c.fi-=t;
if(c.fi<=-t and c.se<=0) c.se+=c.fi/t,c.fi%=t;
return c;
}
friend bool operator <(pii a,pii b){
if(a.se!=b.se) return a.se<b.se;
return a.fi<b.fi;
}
friend pii max(pii a,pii b){
if(a<b) return b;
return a;
}
};
-
cin效率低的原因:默认cin与stdin总是保持同步,cin会把要输出的东西先存入缓冲区,进而消耗时间。通过关闭同步,可以有效提高cin效率;
-
默认情况下cin绑定的是cout,每次执行<<的时候都要调用flush,进而增加IO负担,因此可以通过tie(0)解绑。
链表
struct Node{
Node *pre, *nxt;
int value;
}
void ins(Node *x, Node *y){ //insert y to x->nxt
if(x->nxt!=NULL) y->nxt=x->nxt, y->nxt->pre = y;
x->nxt=y;
}
void del(Node *x){ //delete x
if(x->pre!=NULL) x->pre->nxt=nxt;
if(x->nxt!=NULL) x->nxt->pre=pre;
free(x) //gc
}
栈与队列
手写实现栈与队列
struct Stack{
int a[100010];
int top;
void init(){top=0;}
void push(int x){a[++top]=x;}
void pop(){if(top)top--;}
int size(){return top;}
int query(){return a[top]}
};
struct Queue{
int q[100010];
int l,r;
void init(){l=0;r=0;}
void push(int x){
if(r==100000)r=1;else r++;
q[r]=x;
}
void pop(){if(l==100000)l=1;else l++;}
int front(){return q[l];}
int size(){if(l<r)return r-l;else return l-r+1;}
bool empty(){return l==r;}
};
单调栈
单调队列
来一个题:给一个序列,求所有区间长度为L的区间最大最小值。
在队列中维护一个单调性,即维护这L个元素的最大最小值,O(n)。
const int N=5000010 // 单调增队列
int q[N],l=1,r=1,a[N],mx,mn,inq[N],n,m;
inline void ins(int x){
while(l<r&&q[r-1]<=a[x]) r--; //队列不为空,且a[x]大
q[r]=a[x];inq[r]=x;r++; //记录新元素在原始数组中的索引
}
inline int getmax(int cur){
for(l<r;l++)if(cur-inq[l]<m)return q[l];
}
单调队列性质
- 队列中的元素其对应在原来的列表中的顺序必须是单调递增的。
- 队列中元素的大小必须是单调递(增/减/甚至是自定义也可以)
#include <stdio.h>
const int N=1000000+10;
inline int read(){
int x=0,f=1;char ch;
do{ch=getchar();if(ch==45)f=-1;}while(ch<48||ch>57);
do{x=x*10+ch-48;ch=getchar();}while(ch>=48&&ch<=57);
return f*x;
}
struct Queue{
int q[N]; int l,r; int p[N];
void init(){l=1,r=0;}
void push(int x){q[++r]=x;}
void pop_front(){l++;}
void pop_back(){r--;}
int front_val(){return q[l];}
int rear_val(){return q[r];}
bool empty(){return l>r;}
int xy_front(){return p[l];}
void xy_fv(int i){p[r]=i;}
}Q;
void monotone_min(int a[],int n,int k){
Q.init();
for(int i=1;i<=n;i++){
while(!Q.empty()&&Q.rear_val()>=a[i]) Q.pop_back();
Q.push(a[i]);
Q.xy_fv(i);
while(Q.xy_front()<=i-k) Q.pop_front();
if(i>=k) printf("%d ",Q.front_val());
}
printf("\n");
}
void monotone_max(int a[],int n,int k){
Q.init();
for(int i=1;i<=n;i++){
while(!Q.empty()&&Q.rear_val()<=a[i]) Q.pop_back();
Q.push(a[i]);
Q.xy_fv(i);
while(Q.xy_front()<=i-k) Q.pop_front();
if(i>=k) printf("%d ",Q.front_val());
}
printf("\n");
}
int main(){
int n=read(),k=read();int a[N];
for(int i=1;i<=n;i++) a[i]=read();
monotone_min(a,n,k);
monotone_max(a,n,k);
return 0;
}
优先队列
在弹出的时候,不是按照进入时间,而是按照某种给定的优先级弹出。
最常见的堆是二叉堆,priority_queue定义了一个以权值为优先级的堆。
#include<queue> priority_queue<数据类型>
重载运算符 Kluskal·改 a.w>b.w
例题
一般用来做什么
STL 平衡树
stl平衡树的一些应用
Set
Map
例题: P3307 字符串哈希
Vector
哈希表
P3370 【模板】字符串哈希 - 洛谷 | 计算机科学教育新生态
哈希的过程,其实可以看作对一个串的单向加密过程,并且需要保证所加的密不能高概率重复。
对于每个串,通过一个固定的转换方式,使相同串的“密”相同,不同的串 尽量 不同。
最常见的一种哈希:进制哈希:核心便是给出一个固定进制base,将这个串映射为base进制的数,即为该串的哈希值;通过比对每个串的的哈希值,即可判断两个串是否相同
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
int prime=233317; // 一个非常大的质数
ull mod=212370440130137957ll;
ull hashe(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod+prime; // 哈希映射
return ans;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",s);
a[i]=hashe(s);
}
sort(a+1,a+n+1);
for(int i=1;i<n;i++)
{
if(a[i]!=a[i+1])
ans++;
}
printf("%d",ans);
}
哈希冲突避免
1、无错哈希
对于每一个新的哈希值,我们都可以来判断是否和已有的哈希值冲突,如果冲突,那么可以将这个新的哈希值不断加上一个大质数,直到不再冲突(比如somebody’s birthday)
for(int i=1;i<=m;i++)//m个串
{
cin>>str;//下一行的check为bool型
while(check[hash(str)]) hash[i]+=20000123;
hash[i]+= hash(str) ;
}
2、多重哈希
使用不同的两种或多种方式哈希,然后分别比对每一种哈希值是否相同
字典树
---洛谷题解qip101大佬
Trie 树,即字典树(前缀树),是一种树形结构。典型应用是用于统计和排序大量的字符串前缀来减少查询时间,最大限度地减少无谓的字符串比较。
Trie 树的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
1.根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3.每个节点的所有子节点包含的字符都不相同。
操作
映射字符
int getnum(char x){
if(x>='A'&&x<='Z')
return x-'A';
else if(x>='a'&&x<='z')
return x-'a'+26;
else
return x-'0'+52;
}
插入字符
void insert(char str[])
{
int p=0,len=strlen(str);
for(int i=0;i<len;i++)
{
int c=getnum(str[i]);
if(!t[p][c])
t[p][c]=++idx;
p=t[p][c];
cnt[p]++;
}
}
查询字符
int find(char str[])
{
int p=0,len=strlen(str);
for(int i=0;i<len;i++)
{
int c=getnum(str[i]);
if(!t[p][c])
return 0;
p=t[p][c];
}
return cnt[p];
}
主函数调用
int main(){
scanf("%d",&T);
while(T--)
{
for(int i=0;i<=idx;i++)
for(int j=0;j<=122;j++)
t[i][j]=0;
for(int i=0;i<=idx;i++)
cnt[i]=0;
idx=0;
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
{
scanf("%s",s);
insert(s);
}
for(int i=1;i<=q;i++)
{
scanf("%s",s);
printf("%d\n",find(s));
}
}
return 0;
}
图与树
图论
存储与遍历
-
存储
-
邻接矩阵
-
邻接表
-
链式前向星
head数组存储以该点为起点最后加入的一条边。
next数组存储以该点位起点的上一条边。(编号)
-
-
-
遍历
- 深度优先搜索
- 广度优先搜索
存图方式
链式前向星
只需要开一个数组存储每个点引出的第一条边,然后存储每个点作为起点的每条边,这样就可以做到不重不漏。
定义一个结构体、一个数组和一个变量
struct EDGE
{
int next;
int to;
}edge[1000000];
int head[1000000];
int cnt=0;//指针
起点是用head存储的,其(带权重)构建的完整代码如下
#include<iostream>
using namespace std;
struct edge
{
int next;
int to;
int w;
}edge[MAXM];
int head[MAXN];//head[i]为i点的第一条边
int cnt=0;
void addedge(int u,int v,int w) //起点,终点,权值
{
edge[++cnt].next=head[u];//更新cnt
edge[cnt].to=v;
edge[cnt].w=w;
head[u]=cnt;
}
int main()
{
int n;
for(int i=1;i<=n;i++)
{
int a,b,w;
addedge(a,b,w);
//如果是无向图,还要addedge(b,a,wei);
}
}
这里的next指的是遍历时的下一条边,head指的是遍历时的第一条边,而存边时相当于反过来操作,所以next记录上一条边,而head记录最后一条边。
边的遍历
for(int i=head[x];i!=0;i=edge[i].next)
这个循环的结束条件是i等于0,因为最后一条边,也就是存边时第一条边,在把head值存进next时,head还没有更新过,也就是0。所以当next返回0时,就说明这些边遍历完毕了。
单源点最短路径
BFS灌水 usaco火情蔓延
Floyd
Floyd O(n^3)
-
传递闭包 : a->b,b->c => a->c 差分约束 a>b,b>c => a>c
- 推广 : a - dis1 - b, b - dis2 - c => a - dis1 + dis2 = c
- 适用于有向图、无向图或负权边
-
初始化
- 不可达dis设为 inf = 0x3f3f3f3f
- 自己到自己的距离为0
- 题目给定的边直接赋值为该边长度,双向边两边同时赋值
-
思想
- 枚举中转节点k
- 检查由S经过此点到T的路径是否比原先更优
- 更新由S到T的最短距离
-
例题
-
P1828 香甜的黄油
-
统计每个牧场奶牛数量,Floyd之后再枚举每个牧场作为最终牧场,计算迁移距离,最后取最小值。
-
P2419 牛大赛
- 胜利方向失败方连边,Floyd改为t[i] [j]|=t[i] [k]&t[k] [j]
-
hdu1599 求最小环
-
#include<bits/stdc++.h>
using namespace std;
#define INF 123456789
#define MAXN 10005
inline int read() {...} //快读
int a[MAXN][MAXN],n,m,s;
inline void floyd()
{
for(int k=1;k<=n;k++) //这里要先枚举k(可以理解为中转点)
{
for(int i=1;i<=n;i++){
if(i==k||a[i][k]==INF) continue; //我是你或不可达
for(int j=1;j<=n;j++)
a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
}
}
}
int main()
{
n=read(),m=read(),s=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++) a[i][j]=INF; //边的初始化
for(int i=1,u,v,w;i<=m;i++)
{
u=read(),v=read(),w=read();
a[u][v]=min(a[u][v],w);//边的存储,取min对付重边
}
floyd();
a[s][s]=0;
for(int i=1;i<=n;i++) printf("%d ",a[s][i]);
return 0;
}
SPFA
SPFA O(n^2)
- 能处理负权边的单源最短路径
- 利用队列,多次入队,多次更新最优解(松弛)
- 特点:只有当前被更新的点才对状态产生贡献,队列维护有用状态
用队列来保存待优化的结点(类似于BFS),每次取出队首结点,并用队首节点来对最短路径进行更新并进行松弛操作
如果要对所邻接点的最短路径需要更新,且该点不在当前的队列中,就将该点加入队列
然后不断进行松弛操作,直至队列空为止。
#include<bits/stdc++.h>
using namespace std;
// 快读
inline int read()
{
int x=0,k=1; char c=getchar();
while(c<'0'||c>'9'){if(c=='-')k=-1;c=getchar();}
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*k;
}
#define maxn 10005
#define maxm 500005
#define inf 1234567890
int n,m,s,cnt,dis[maxn],head[maxn];
bool vis[maxn];
// 链式前向星
struct Edge
{
int next,to,w;
}ed[maxm];
void add(int u,int v,int w)
{
ed[++cnt].next=head[u];
ed[cnt].to=v;
ed[cnt].w=w;
head[u]=cnt;
}
queue<int> q;
inline void spfa()
{
for(int i=1; i<=n; i++) dis[i]=inf;//初始化
dis[s]=0; q.push(s); vis[s]=1; //赋初值
int u,v;
while(!q.empty())
{
u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=ed[i].next)
{
v=ed[i].to;
if(dis[u]+ed[i].w<dis[v])
{
dis[v]=dis[u]+ed[i].w;
if(!vis[v])
vis[v]=1, q.push(v);
}
}
}
}
int main(){
n=read(),m=read(),s=read();
for(int i=1,u,v,w;i<=m;i++)
{
u=read(),v=read(),w=read();
add(u,v,w);
}
spfa();
for(int i=1; i<=n; i++)
{
printf("%d ",dis[i]);
}
return 0;
}
Dijkstra
Dijkstra O(n^2logn)
- 基于贪心的一个单源点最短路径算法,只能处理正权边
- 实现思路
- 将顶点化为两堆,起初第一堆只有起点V0
- 每次从第二堆里距离V0点最近的点取出,放入第一堆,并更新最短路,直到第二堆没有节点
- 此时维护dis[i]从V0点到i点的最小距离
Dijkstra是基于一种贪心的策略,首先用数组dis记录起点到每个结点的最短路径,再用一个数组保存已经找到最短路径的点
然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点记为已经找到最短路
此时完成一个顶点,再看这个点能否到达其它点(记为v),将dis[v]的值进行更新
不断重复上述动作,将所有的点都更新到最短路径 O(N^2) --- > 堆优化 O(NlogN)
#include <iostream>
#define INF 2147483647
#define MAXN 100005
using namespace std;
struct edge {int next, to, w;} ed[MAXN];
int head[MAXN], cnt;
void addedge(int u, int v, int w) {
ed[++cnt].next = head[u];
ed[cnt].to = v;
ed[cnt].w = w;
head[u] = cnt;
}
long dis[MAXN];
bool vis[MAXN];
int main() {
//初始化与链式前向星的构造
int n, m, s; cin>>n>>m>>s;
for (int i=1; i<=n; i++) dis[i] = INF;
dis[s] = 0;
int a, b, c;
for (int i=1; i<=n; i++) {cin>>a>>b>>c; addedge(a,b,c);}
//dijkstra算法核心
int pos = s;
while (!vis[pos]) {
vis[pos] = 1;
for (int i = head[pos]; i; i = ed[i].next) {
if (!vis[ed[i].to] && dis[ed[i].to] > dis[pos] + ed[i].w)
dis[ed[i].to] = dis[pos] + ed[i].w; //更新dis
}
long minn = INF;
for (int i = 1; i <= n; i++) {
if (!vis[i] && dis[i] < minn)
minn = dis[i], pos = i; //换起点
}
}
for (int i = 1; i <= n; i++)
cout << dis[i] << " ";
}
在dis数组中选择最小值时,可以用一些数据结构来进行优化。
堆相对于线段树以及平衡树有着常数小,码量小等优点,并且堆的一个妙妙的性质就是可以在nlogn的时限内满足堆顶是堆内元素的最大(小)值。
#堆优化
struct node
{
int w,now;
inline bool operator <(const node &x)const
//重载运算符把最小的元素放在堆顶(大根堆)
{
return w>x.w;//这里注意符号要为'>'
}
};
priority_queue<node>q;
void dijkstra(){
...
//赋初值
q.push((node){0,s});
while(!q.empty()){
//堆为空即为所有点都更新
node x=q.top();
q.pop();
int u=x.now;
//记录堆顶(堆内最小的边)并将其弹出
if(vis[u]) continue;
//没有遍历过才需要遍历
vis[u]=1;
for(int i=head[u];i;i=e[i].next)
//搜索堆顶所有连边
{
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w)
{
dis[v]=dis[u]+e[i].w;
//松弛操作
q.push((node){dis[v],v});
//把新遍历到的点加入堆中
}
}
}
int main(){
...
dijkstra();
...
}
}
**有向无环图 DAG **
拓扑排序
- 常被用来表示事件之间的依赖关系,管理任务之间的调度
- 实现思路
- 用一个数组记录每个点初始状态下的入度
- 将入度为0的点加入宽度搜索的队列
- 对于当前点,将其出边对应的所有点的入度都减1,然后判断入度是否为0,如果是,就将该点加入队尾
- 例题
- P2261 信息传递
- 先拓扑排序找到所有环
- 然后dfs每个环,求得节点最少的环
- P2261 信息传递
最小生成树 MST
prime
-
prime O(N^2)
Kluskal
-
Kluskal
树
树的相关知识
- 树的存储方式:
- 父亲节点表示法 : fa[x] x节点的父亲
- 儿子节点表示法 : son[n] [n]
- 无向图表示 : 邻接矩阵,邻接表
- 左儿子右兄弟 - 树转二叉树
- 树的重心和直径
-
直径:树上的最长路径
- 计算思路
- ① 每个点dfs
- ②树上任选点u,求距离u最远的点y,再求离y最远点s,y到s距离为树的直径
- 计算思路
-
重心:找到一个点,其所有子树中 最大的子树节点数最少的点
-
最近公共祖先 LCA
自平衡二叉搜索树
红黑树
定义:一种高效的自平衡二叉搜索树 (来自可爱的本子喵)o(=•ェ•=)m
前置知识
二叉搜索树:
-
左子树的所有节点值均<=父节点的值
-
右子树的所有节点值均>=父节点的值
-
左右子树也分别为二叉搜索树
普通二叉树的缺点:插入【13,14,15,16】,最坏时间复杂度O(N)
为了避免出现这种情况,提出平衡树结构,红黑树是其中一种
红黑树查找、删除、插入时间复杂度O(logN)
插入: 234树 --- 红黑树的前身
红黑树的性质:【 左根右,根叶黑,不红红,黑路同 】
- 节点是红色或黑色
- 根节点是黑色
- 叶子节点(最底层不存放数据的节点)都为黑色,且都为空
- 红色节点的父节点and子节点都为黑色(不会存在两个连续的红色节点)
- 从任一节点到叶子节点的所有路径都包含相同数量的黑色节点
红黑树与234树等价(将红黑树所有红色节点上溢到与它们父节点同一高度---234树)
红黑树的黑色节点个数=234树的节点数
234树的每一个节点中:
- 黑色节点必为父节点,红色节点为子节点(黑色中间,红色两边)
二叉搜索树的旋转
将X的左儿子指向2,将Y的右儿子那条线擦掉,然后Y指向X
插入
如果插入的是根节点,则为黑色
其余情况插入的节点最开始一定为红色
如果是插入红色节点,仅有一种冲突状况:可能出现连续两个红色节点,这时候需要旋转和变色来调整
红黑树的插入操作分为12种情况
其中,插入节点的父节点为黑色的4种情况,是可以直接插入,不做调整的
情况一:4种可以直接插入
情况二:4种染色+旋转【叔父节点不是红色】之 LL/LR
情况二:4种染色+旋转【叔父节点不是红色】之 RL/LR
情况三:4种染色【叔父节点是红色】上溢
删除
以234树为基础理解红黑树,1个B树节点=以黑色节点为主,若有红色节点则在其两边
B树中的删除操作 对于非叶子节点,是转换为其前驱/后继节点的删除
比如要删除6,则选择其前驱节点8或者后继节点10进行交换,然后删除前驱/后继节点
删除操作分为两大类:
-
红色节点:可以直接删除
-
黑色节点:必须进行调整以满足红黑树的性质---从任一节点到叶子节点的所有路径都包含相同的黑色节点
-
有2个红色子节点的黑色节点(不做考虑) 234树的4节点
-
有1个红色子节点的黑色节点 234树的3节点
用唯一的红色子节点来替代被删除的节点
-
黑色叶子节点(下溢) 234树的2节点
①删除节点为根节点,直接删除
②删除节点的兄弟节点为黑色
1、兄弟节点有红色子节点(借用兄弟子节点修复) 【 删除36 】
在第一种情况下:
在第二种情况下:
在第三种情况下:可以随意选择左子节点或右子节点
综上,步骤总结:
-
在删除节点后进行旋转
-
中心节点染为父节点的颜色
-
两个子节点染为黑色
2、兄弟节点没有红色子节点(父节点向下合并)
兄弟节点染红,父节点染黑
如果父节点为黑色:把父节点当作已被删除的节点处理,递归
③删除节点的兄弟节点为红色(转为黑色处理)
-
-
父节点右旋,兄弟节点染黑,父节点染红,然后使用兄弟为黑色的方法进行修复
基础动态规划
DP概念
-
最对最优化问题,确定最优解条件,将完整问题划分成多个阶段
-
动规问题必须包含最优子结构 : 由n-1的最优推导n的最优
-
贪心:先做决策,只顾眼前利益;动规:基于当前最优推导出全局最优
-
例题 斐波那契数列 求个位数
线性模型
最长上升子序列 LIS
-
解释:如同1 3 0 2 6 7 9序列中的1 2 6 7 9,按顺序取出这些数,后一个数总比前一个数大的序列叫做这个序列的上升子序列。
-
题目:对于一个个数为n的序列,求它的最长上升子序列。
-
解法:动态规划,部分最优状态“优上加优”,设f[i]表示以a[i]结尾的最长上升子序列的长度,核心代码如下:
for(int i=1;i<=n;i++){ f[i]=1; for(int j=1;j<i;j++){ if(a[i]>a[j]) //上升 f[i]=max(f[i],f[j]+1); //最长子序列 } }
变体
例题1
解题
- 编号 1... ti ... n
- 分别求最长上升和最长下降,然后枚举ti
例题2 P2782 友好城市
解题
-
要求不能相交:排个序然后LIS
例题3 最大上升子序列和
题目:N个数的序列,求最大上升子序列的和
解题
- 定义f[i]表示以a[i]为最后一个数的递增子序列的最大和
- f[i]=max( f[j] ) + a[i] Ans=max
最大连续子段和
-
解释:给定一个序列(正负不定),求连续的一段序列,使得它们和最大
-
举例:假设有序列 : 2 3 -1 2 2 -3 1
那么,最大连续字段和是 2 + 3 + (-1) + 2 + 2 = 8
-
求解:f[i] : 以a[i]为终点(连续区间右边界)的子序列的最大和
f[i] = max( f[i-1]+a[i], a[i] ) = max( f[i-1], 0 ) + a[i] //如果左边是负数,没必要加,如果是正数,多多益善
-
例题1 P1982 小朋友的数字
最长公共子序列 LCS
-
解释:两个子串A=abdde,B=abeee,则最长公共子序列C=abe
-
求法:f[i] [j]:A串匹配到了i位,B串匹配到了j位,此时LCS长度为f[i] [j]
当A串第i位和B串第j位相同的时候,f[i] [j]=f[i-1] [j-1]+1
其他时候,f[i] [j]=max( f[i-1] [j], f[i] [j-1] )
坐标类模型
-
概念:状态比较简单,通常f[i] [j]表示i行j列
一般移动方式会直接给出----我从哪里来,要到哪里去?
-
例题1:
例题2: P1004 方格取数
例题3: P1002 过河卒 cv1219 骑士游历 P3126 回文路径
在判断一个点有没有被马拦住时,会用到 (i−2,j−1) 和(i−1,j−2) 这两个位置,那如果不把所有的点的坐标都加上 2 (前面分析的时候只把所有的坐标加上 1),就会因为数组越界而 WA 掉一个点。
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #define ll long long using namespace std; const int fx[] = {0, -2, -1, 1, 2, 2, 1, -1, -2}; const int fy[] = {0, 1, 2, 2, 1, -1, -2, -2, -1}; //马可以走到的位置 int bx, by, mx, my; ll f[40][40]; bool s[40][40]; //判断这个点有没有马拦住 int main(){ scanf("%d%d%d%d", &bx, &by, &mx, &my); bx += 2; by += 2; mx += 2; my += 2; //坐标+2以防越界 f[2][1] = 1;//初始化 s[mx][my] = 1;//标记马的位置 for(int i = 1; i <= 8; i++) s[mx + fx[i]][my + fy[i]] = 1; for(int i = 2; i <= bx; i++){ for(int j = 2; j <= by; j++){ if(s[i][j]) continue; // 如果被马拦住就直接跳过 f[i][j] = f[i - 1][j] + f[i][j - 1]; //状态转移方程 } } printf("%lld\n", f[bx][by]); return 0; }
区间模型
背包模型
01背包
01背包问题:有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品只能取1次放入背包中,背包所有物品权重和最大是多少?
// 01背包
#include <iostream>
using namespace std;
#define MX 50010
long n, m;
long v[MX], w[MX], f[MX];
long max(long x, long y) {
return x > y ? x : y;
}
int main() {
cin >> n >> m;
for (long i = 1; i <= n; i++)
cin >> w[i] >> v[i];
for (long i = 1; i <= n; i++)
for (long j = m; j >= w[i]; j--)
f[j] = max(f[j], f[j - w[i]] + v[i]);
cout << f[m];
return 0;
}
完全背包
完全背包 :有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品可以取无限次放入背包中,背包所有物品权重和最大是多少?
01背包问题和完全背包问题的区别就在于,每个物品取的最大次数是1次还是无限次。
算法之动态规划(DP)求解完全背包问题_动态规划法求解完全背包问题-CSDN博客
for (int i = 1; i <= n; i ++ ) {
scanf("%d %d", &w, &v);
for (int j = w; j <= m; j++ ) {
f[j] = (f[j] > f[j - w] + v) ? f[j] :f[j - w] + v;
}
}
printf("%d\n", f[m]);
-
多重背包
-
混合背包
分组背包
// 分组背包
#include <iostream>
using namespace std;
#define MX 50010
#define MY 10010
long n, m, z, t; // 一共t组,z是中间量
long v[MX], w[MX], f[MX], b[MX]; // b是一个桶
long g[MY][MY];
long max(long x, long y) {
return x > y ? x : y;
}
int main() {
cin >> m >> n; // 总重m,总共n个
for (long i = 1; i <= n; i++) {
cin >> w[i] >> v[i] >> z;
t = max(t, z);
b[z]++;
g[z][b[z]] = i;
}
for (long i = 1; i <= t; i++)
for (long j = m; j >= 0; j--)
for (long k = 1; k <= b[i]; k++)
if (j >= w[g[i][k]])
f[j] = max(f[j], f[j - w[g[i][k]]] + v[g[i][k]]);
cout << f[m];
return 0;
}
-
依赖背包
-
二维背包与多维费用背包
搜索
一个例子:素数环
素数环EX,既然每一层for循环都很像,那么何不让程序自己去处理呢?
回溯法
要点: 初始状态,目标状态,穷举范围,约束条件,状态恢复
代码框架:
-
先判断是否达到目标状态
-
如果达到,判断当前状态是否合法、 是否计入答案
-
未达到,枚举可能的状态---标记本轮选择,进入下一层
-
---标记返还
DFS-栈
bfs求解素数环
int a[30],vis[20],n,ans;
inline bool check(int x){
for(int i=2;i*i<=x;i++) if(x%i==0)return 0;
return 1;
}
inline void dfs(int now){
if(now==n+1){ // now 标定了目标状态,一共now个
if(check(a[n]+a[1]))ans++;
return;
}
for(int i=1;i<=20;i++){ // a[now-1]就是前一个
if(vis[i])continue; // i就是下一个a[now]
if(!check[i+a[now-1]])continue;//寻找满足条件的a[now]
vis[i]=1;a[now]=i;
dfs(now+1);
vis[i]=0;
}
}
int main(){
n=read();
dfs(1);
printf("%d\n",ans);
}
一个例题: P1238 走迷宫 dfs实现
一个应用:全排列
一个新问题:P1135 奇怪的电梯
BFS-队列
考虑这种遍历的形式:
- 得到一个状态后,用它拓展出所有状态,然后将它丢弃
- 每次选择当前未被丢弃的最早的一个状态进行拓展,这样就能保证优先的性质
奇怪的电梯求解
P1238 走迷宫 bfs实现
二者比较
bfs:具有良好的优先性质,但较难统计具体方案
dfs:易于保存方案,首选,但无法解决优先性问题,依赖系统栈会致使栈溢出
搜索的时间复杂度
一些练习题
P1037 产生数
考虑dfs解决:首先遍历每一位,按照规则生成改位新的数进行搜索,由于映射关系不存在自环,不需要判重。
P1451 求细胞数量
这是一种常见的搜索办法,被称为染色法或flood-fill算法。本质是bfs或dfs
优化搜索
一道老题
我们有几个比较基础的判断:
是否做到了m层,是否最终体积为0,是否当前面积最小
优化方向
-
最优化剪枝
如果当前表面积+余下侧面积>之前的最优值,直接返回
-
可行性剪枝
如果当前剩下的做不到m层,肯定要跳出
同理,如果剩下的太多,哪怕以最大代价做完m层都会用剩,也要跳出
剪枝的注意点
总结
经典剪枝例题: 骑士巡游问题
记忆化搜索
记忆化搜索例题 P3183 食物链
贪心与二分
二分
inline double find(double l, double r){
while(r-l>eps){ //eps精度
double mid=(l+r)/2;
if(f(mid)==0)return mid;
if(f(l)*f(mid)<0)r=mid;
else l=mid;
}
return l;
}
二分查找
inline int Binary_Search(int l,int r,int x){
if(l>r)return -1;
int mid=(l+r)>>1;
if(a[mid]==x)return mid;
if(x<a[mid])return Binary_Search(l,mid-1,x);
if(x>a[mid])return Binary_Search(mid+1,r,x);
}
STL的二分查找
-
在vector上的用法
lower_bound(a.begin(), a.end(), 2)
vector的迭代器可减,可以和数组一样减去数组名来获取下标
-
在map、set上的用法
a.lower_bound(2)
STL的迭代器不可减,毕竟这本质是平衡树,没有下标
二分答案
二分答案的模板
例题1 NOIP2011 聪明的质检员
解释:每次对给定的W,≥W的个数和乘上价值和,检验值y是所有区间检验值的和,现在的任务就是调整W使得得到的结果尽可能接近S
思路:寻找单调性---W越大,y越小,换言之,y=f(W)是一个单调函数。此时问题转变成abs(f(W)-s)的最小值,求零点或者接近零点的点,二分解决。那么如何快速计算f(w)?可以先离线求出所有矿石价值和个数的前缀和,这样可以做到一个O(1)查询一个区间,总复杂度O(nlogn)
例题2 NOIP2012 借教室
例题3 NOIP2015 跳石头
要求移除至多M块石头,使得选手跳跃最短距离尽可能长
寻找单调性,移除石头越多,答案一定越大,令f(x)表示当最小值不低于x时,最小需要移动多少石头,对这个函数二分答案即可。
那么怎么求这个f(x)呢?
贪心
和动规进行比较
什么样的题可以贪心
小栗子
dp如何设计
考虑贪心来求解
小结
大梨子
再次感受一下贪心算法
例题 NOIP2004 合并果子
例题 P1903 活动选择问题
例题 NOIP2010 三国问题
例题 P1223 排队接水
例题 P1012 拼数
运用贪心思想的经典算法
哈夫曼编码
- 任何一个编码都不是其他编码的前缀
- 尽量用少的字节
数据结构——哈夫曼(Huffman)树+哈夫曼编码 - 王陸 - 博客园
[译文]5分钟系列—快速理解Huffman Coding(霍夫曼编码) - 知乎
编码 do or not do
解码
引例:
编码问题:
-
P5000 Hillwer编码 - 洛谷 | 计算机科学教育新生态
-
GG
-
-
P1246 编码 - 洛谷 | 计算机科学教育新生态 (升序)
-
#include<bits/stdc++.h> using namespace std; string s; int ans,n; int c(int m,int n)//组合数计算 { if(m==0)return 1; int mut=1; for(int i=n;i>n-m;i--)mut*=i; for(int i=m;i>1;i--)mut/=i; return mut; } int main() { cin>>s,n=s.size(); for(int i=1;i<n;i++) if(s[i]<=s[i-1])cout<<0,exit(0);//判断是否存在 for(int i=1;i<n;i++)ans+=c(i,26);//计算出位数比它小的单词数 for(int i=0;i<n;i++)//枚举每一位 for(char j=(i==0?'a':s[i-1]+1);j<s[i];j++)//注意考虑边界 ans+=c(n-i-1,'z'-j);//因为字符串下标从0开始,所以是n-i-1而不是n-i cout<<++ans;//别忘了最后把自己加上 return 0; }
-
// 递推 杨辉三角 #include<iostream> using namespace std; string s; int f[30][10],ans,cnt; int main(){ cin >> s; for(int i = 1;i < s.size();i++){ if(s[i - 1] >= s[i]){ cout << 0; return 0; } } //判断字符串是否合法(升序) for(int i = 1;i <= 26;i++) f[i][1] = 1; //长度为1的字符串数量都是1 for(int j = 2;j <= 6;j++) for(int i = 27 - j;i > 0;i--) f[i][j] = f[i + 1][j - 1] + f[i + 1][j]; //由公式,我们从上至下从右至左进行计算 for(int j = s.size() - 1;j >= 0;j--){ cnt++; for(int i = 1;i <= s[j] - 'a' + 1;i++) ans += f[i][cnt]; } //由以上的思路计算答案 cout << ans; return 0; }
-
例题 P1230 智力大冲浪
模拟与枚举
模拟
例题 NOIP2003 普及组] 乒乓球
模拟题注意点: 仔细读题,所有规则都从题目中提取,及时记录特判
例题 NOIP2017 时间复杂度
枚举
例题 NOIP2011 提高组] Mayan 游戏
枚举策略
为什么学枚举策略
BFS
哈希
例题 P1379 八数码难题
Two Pointers
折半查找
正难则反
提取信息
优化搜索
[P3417 POI2005] BANK-Cash Dispenser - 洛谷 | 计算机科学教育新生态
大事化小
据问减枝
基础数学思维
组合计数
计数原理
排列组合
集合
定义运算
子集
数论
素数
素数判定
bool isRrime(int n){
for(int i=2;i*i<=n;i++)
if(n%i==0) return false;
return true;
}
筛素数
例题:第K小素数
埃氏筛
//生成不大于 n 的所有质数
bool numlist[100000001];
int prime[20000001], cnt;
void work(int n){
for(int i=2; i<=n; i++){
if(numlist[i]==false){
prime[++cnt] = i ;
for(int j=i; i*j<=n; j++)
numlist[i*j] = true;
}
}
return;
}
要想得到 n 以内的质数,就要把不大于 n 的质数的倍数全部剔除,剩下的就是质数。从 2 开始,把 2 的倍数(不包括本身)标记为合数,然后向后枚举,查到一个未标记为合数的,就把它的倍数(不包括本身)标记为合数。以此类推,查到 n 为止。
有一个小优化,把 t 的倍数标记为合数时,不是从 2t 开始,而是从 t^2 开始(因为小于 t^2 的 t 的倍数在枚举到 t 前就被标记过了)。
当然,埃氏筛效率还是低了些。例如一个数 24,它会被 2, 3, 4 三个数标记,这就重复了两次,更大的数同理。时间复杂度 O(nlogn)。
欧拉筛
bool numlist[100000001];
int prime[20000001], cnt;
void work(int n){
for(int i=2; i<=n; i++){
if(numlist[i]==false)
prime[++cnt] = i ;
for(int j=1; j<=cnt&&i*prime[j]<=n; j++){ //最大非自身因数
numlist[i*prime[j]] = true ; //记忆化存储
if(i%prime[j]==0) //最小质因数
break;
}
}
return; //一根筋变两头堵
}
避免重复筛,应找到筛合数的一种原则:这个合数只会被它的最大非自身因数(对应最小质因数)筛。这样能保证每个合数只会被筛一次。
以下为完整代码
#include <cstdio>
#include <cstring>
bool isPrime[100000010];
//isPrime[i] == 1表示:i是素数
int Prime[6000010], cnt = 0;
//Prime存质数
void GetPrime(int n)//筛到n
{
memset(isPrime, 1, sizeof(isPrime));
//以“每个数都是素数”为初始状态,逐个删去
isPrime[1] = 0;//1不是素数
for(int i = 2; i <= n; i++)
{
if(isPrime[i])//没筛掉
Prime[++cnt] = i; //i成为下一个素数
for(int j = 1; j <= cnt && i*Prime[j] <= n/*不超上限*/; j++)
{
//从Prime[1],即最小质数2开始,逐个枚举已知的质数,并期望Prime[j]是(i*Prime[j])的最小质因数
//当然,i肯定比Prime[j]大,因为Prime[j]是在i之前得出的
isPrime[i*Prime[j]] = 0;
if(i % Prime[j] == 0)//i中也含有Prime[j]这个因子
break; //重要步骤。见原理
}
}
}
int main()
{
int n, q;
scanf("%d %d", &n, &q);
GetPrime(n);
while (q--)
{
int k;
scanf("%d", &k);
printf("%d\n", Prime[k]);
}
return 0;
}
GCD 与 LCM
最大公约数 gcd 与 最小公倍数 lcm
int gcd(int a, int b){
if(a>b) swap(a, b);
if(a==o) return b;
else return gcd(a, b%a);
}
int gcd(int a,int b){return a==0?b:gcd(a,b%a);}
例题:
幂运算
原理 --- log2(b) P1226 【模板】快速幂 - 洛谷 | 计算机科学教育新生态
快速幂
(1)如果将 a 自乘一次,就会变成 a^2 。再把 a2自乘一次就会变成a4 。然后是 a^8…… 自乘 n 次的结果是 a2n 。
(2)a^x * ay=a(x+y)。
(3)将 b 转化为二进制: b(11)10 = b(1011)2 = 8 + 2 + 1。
int quickPower(int a, int b)//是求a的b次方
{
int ans = 1, base = a;//ans为答案,base为a^(2^n)
while(b > 0)//b是一个变化的二进制数,如果还没有用完
{
if(b & 1)//&是位运算,b&1表示b在二进制下最后一位是不是1,如果是:
ans *= base;//把ans乘上对应的a^(2^n)
base *= base;//base自乘,由a^(2^n)变成a^(2^(n+1))
b >>= 1;//位运算,b右移一位,如101变成10(把最右边的1移掉了),10010变成1001。现在b在二进制下最后一位是刚刚的倒数第二位。结合上面b & 1食用更佳
}
return ans;
}
/*关于 b & 1:
“&”美名曰“按位与”。
二进制
b = 1011
1 = 0001
b&1 = 0001
因为 1(二进制)的前面几位全部都是 0,
所以只有 b 二进制最后一位是 1 时,b & 1 才会返回 1。 */
取余运算
快速幂经常要结合取余运算。这里也讲一点。
取余运算有一些好用的性质,包括:
(A + B) mod b= (A mod b + B mod b) mob b
(A x B) mod b= ((A mod b) x (B mod b)) mob b
#include <iostream>
using namespace std;
int main() {
long long a, b, q;
cin >> a >> b >> q;
long long ans = 1, base = a, b_copy = b;
while (b > 0) {
if (b & 1) {
ans *= base;
ans %= q;
}
base *= base;
base %= q;
b >>= 1;
}
cout << a << "^" << b_copy << " mod " << q << "=" << ans;
return 0;
}
矩阵快速幂
矩阵乘法
//本例中计算矩阵A×矩阵B,存到矩阵C里
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){ //找到c[i][j],开始算
for(int k=1;k<=n;k++){
c[i][j]+=a[i][k]*b[k][j];//矩阵乘法的定义
}
}
}
另一种快速幂
while(p>0) {
if(p%2!=0) ans=ans*b;
b=b*b;
p=p>>1;
}
矩阵乘法 + 快速幂
struct Matrix { //结构体
long long c[101][101];
} A,I;
long long n,k;
Matrix operator*(const Matrix &x,const Matrix &y) { //重载
Matrix a;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
a.c[i][j]=0;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
for(int k=1; k<=n; k++) {
a.c[i][j]+=x.c[i][k]*y.c[k][j]%Mod;
a.c[i][j]%=Mod;
}
return a;
}
void Fastpower_Matrix(int k){
for(int i=1; i<=n; i++)
I.c[i][i]=1; //单位矩阵定义
while(k>0) { //Fastpower_Matrix
if(k%2==1) I=I*A;
A=A*A;
k=k>>1;
}
}
完整代码
#include <iostream>
using namespace std;
const int Mod = 1000000007;
long long n, m;
struct Matrix {
long long c[101][101];
} A, I;
Matrix operator*(const Matrix &x, const Matrix &y) {
Matrix a;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
a.c[i][j] = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
for (int k = 1; k <= n; k++) {
a.c[i][j] += x.c[i][k] * y.c[k][j] % Mod;
a.c[i][j] %= Mod;
}
return a;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cin >> A.c[i][j];
for (int i = 1; i <= n; i++)
I.c[i][i] = 1;
while (m > 0) {
if (m & 1) // 第一种快速幂
I = I * A;
A = A * A;
m = m >> 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
cout << I.c[i][j] << ' ';
cout << endl;
}
return 0;
}
前缀和
概念:
模板:
using ll = long long;
const ll N = 1e5 + 9;
ll a[N], prefix[N];
int main(){
ll n;cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n.i++) prefix[i]=prefix[i-1]+a[i];
}
拓展:
①带有逆的运算
求一段区间的异或和(亦或的逆运算是它自己,因此pre[r] ^ pre[l-1]即可 )。
②带修改的前缀和
③二维的前缀和
P3406 海底高铁
int n, m, a, b, c, p[N], s[N], ans;
int main() {
n = read(); m = read();
for (int i = 1; i <= m; i++) p[i] = read();
for (int i = 1; i < m; i++) {
s[max(p[i], p[i + 1])]--; // 构建差分数组
s[min(p[i], p[i + 1])]++; // 此时l=r,区间长度为1
}
for (int i = 1; i <= n; i++)
s[i] += s[i-1]; //s[i]:从1到i的累计次数
for (int i = 1; i < n; i++) {
a = read(); b = read(); c = read();
ans += min(a*s[i], b*s[i]+c);
}
cout << ans;
return 0;
}
/*没有AK,过了60%,把int 换成long long就过了*/
原题解
#include<iostream>
#define max(a,b) a>b?a:b
#define min(a,b) a<b?a:b
using namespace std;
int main(){
long long n,m,x,y,ans=0;
cin>>n>>m;
long long p[m+1];
long long t[n+1]={};
long long a[n+1],b[n+1],c[n+1];
for(long long i=1;i<=m;i++){
cin>>p[i];
}
for(long long i=1;i<=n-1;i++){
cin>>a[i]>>b[i]>>c[i];
}
for(long long i=1;i<=m-1;i++){
x = min(p[i],p[i+1]);
y = max(p[i],p[i+1]);
t[x]++;
t[y]--;
}
for(long long i=1;i<=n;i++){
t[i]+=t[i-1];
}
for(long long i=1;i<=n-1;i++){
ans+=min(a[i]*t[i],b[i]*t[i]+c[i]);
}
cout<<ans;
return 0;
}
差分
概念:
模板:
using ll = long long;
const ll N = 1e5 + 9;
ll a[N], diff[N], prefix[N];
int main(){
ll n;cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n.i++) diff[i]=a[i]-a[i-1];
}
ll m; cin>>m;
while(m--){
ll l,r; cin>>l>>r>>x;
diff[l]+=x, diff[r+1]+=x; //差分
}
for(int i=1;i<=n;i++) a[i]=a[i-1]+diff[i]; //前缀和还原
for(int i=1;i<=n;i++) prefix[i]=prefix[i-1]+a[i];
while(q--){
ll l,r; cin>>l>>r;
cout<<prefix[r]-prefix[l-1]<<'\n'; //指令输出
}
并查集
处理一些不相交集合的合并与查询问题
Find:确定元素属于哪一个子集,可被用来确定两个集合是否属于同一子集
Union:将两个子集合并成同一个集合
#include <iostream>
#include <stdio.h>
const int N=10000+10;
using namespace std;
int fa[N],n,m;
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
void merge(int x,int y){
int xx=find(x),yy=find(y);
fa[yy]=xx;
}
inline int read(){
...
}
int main(){
n=read();m=read();
for(int i=1;i<=n;i++) fa[i]=i;
while(m--){
int opt=read(),x=read(),y=read();
if(opt==1)merge(x,y);
else{
int xx=find(x),yy=find(y);
xx==yy?puts("Y"):puts("N");
}
}
return 0;
}
例题: P1551 亲戚 P1197 星球大战
并查集在常见算法中的应用
KMP
#include<iostream>
#include<cstring>
#define MAXN 1000010
using namespace std;
int kmp[MAXN];
int la, lb, j;
char a[MAXN], b[MAXN];
int main(){
cin>>a+1;
cin>>b+1;
la = strlen(a+1);
lb = strlen(b+1);
for(int i=2; i<=lb; i++){
while(j&&b[i]!=b[j+1]) j = kmp[j];
if(b[i]==b[j+1]) j++;
kmp[i] = j;
}
j = 0;
for(int i=1; i<=la; i++){
while(j>0&&a[i]!=b[j+1]) j = kmp[j];
if(a[i]==b[j+1]) j++;
if(j==lb){
cout<<i-lb+1<<endl;
j = kmp[j];
}
}
for(int i=1; i<=lb; i++)
cout<<kmp[i]<<" ";
return 0;
}
第K大的数
std::nth_element()