Noip的时间越来越近了,作为一只蒟蒻,复习的方法当然是写模板咯(雾),骗分过样例,暴力出奇迹,下面请看我一波操作(若有错误请大家指正)。
最小生成树Kruskal
Kruskal适用于处理稀疏图,将边从小到大排序,加边过程用并查集辅助完成
此处以洛谷P3366为例,并查集路径压缩是对Kruskal最基本的优化
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_N=5007;
const int MAX_M=200007;
int n,m;
int ans,edge;
int fa[MAX_N];
struct node{
int x,y,w;
}e[MAX_M];
bool cmp(node p,node q)
{
return p.w<q.w;
}
void init()
{
for(int i=1;i<=n;i++)
{
fa[i]=i;
}
ans=edge=0;
mem(e,0);
}
int get(int x)
{
if(fa[x]==x)
{
return x;
}
return fa[x]=get(fa[x]);//路径压缩
}
void merge(int rootx,int rooty)
{
fa[rootx]=rooty;
}
int main()
{
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].w);
}
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++)
{
int rootx=get(e[i].x);
int rooty=get(e[i].y);
if(rootx!=rooty)
{
ans+=e[i].w;
edge++;
merge(rootx,rooty);
}
}
if(edge!=n-1)
{
printf("orz\n");
}
else
{
printf("%d\n",ans);
}
return 0;
}
最小生成树Prim
Prim算法更适合处理稠密图,基本思想是将已生成的点逐步扩展成为一个集合,根据已有点,刷新计算其余点到整个集合的距离
#include<iostream>
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_N=5007;
const int MAX_M=200007;
const int Inf=0x3f3f3f3f;
int n,m;
int eid,ans;
struct node{
int v,w,next;
}e[MAX_M*2];
int dist[MAX_N],p[MAX_N];
bool vst[MAX_N];
void init()
{
mem(e,0);
mem(p,-1);
mem(vst,0);
mem(dist,0x3f);
eid=ans=0;
}
void insert(int u,int v,int w)
{
e[eid].v=v;
e[eid].w=w;
e[eid].next=p[u];
p[u]=eid++;
}
void insert2(int u,int v,int w)
{
insert(u,v,w);
insert(v,u,w);
}
int main()
{
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
insert2(u,v,w);
}
dist[1]=0;
for(int i=1;i<=n;i++)
{
int min_n=Inf,u=0;
for(int j=1;j<=n;j++)
{
if(!vst[j]&&dist[j]<min_n)
{
min_n=dist[j];
u=j;
}
}
vst[u]=1;
if(u==0) break;
ans+=min_n;
for(int j=p[u];~j;j=e[j].next)
{
int v=e[j].v;
if(!vst[v]&&dist[v]>e[j].w)
{
dist[v]=e[j].w;
}
}
}
printf("%d\n",ans);
return 0;
}
最短路径Dijkstra
最短路径同样也是图论中的经典,Dijkstra与Prim的思想较为相近,选择的是加点的方式,但不同点在于Djikstra刷新的是新点到起点的距离。
此处以洛谷3371为例
#include<iostream>
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int Inf=0x3f3f3f3f;
const int MAX_N=1e4+7;
const int MAX_M=5e5+7;
struct node{
int v,w,next;
}e[MAX_M*2];
int p[MAX_N];
int dist[MAX_M];
bool vst[MAX_N];
int n,m,s;
int eid;
void init()
{
mem(e,0);
mem(p,-1);
mem(vst,0);
mem(dist,0x3f);
eid=0;
}
void insert(int u,int v,int w)
{
e[eid].v=v;
e[eid].w=w;
e[eid].next=p[u];
p[u]=eid++;
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
init();
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
insert(u,v,w);
}
dist[s]=0;
for(int i=1;i<=n;i++)
{
int min_n=Inf,u=0;
for(int j=1;j<=n;j++)
{
if(!vst[j]&&dist[j]<min_n)
{
min_n=dist[j];
u=j;
}
}
vst[u]=1;
for(int j=p[u];~j;j=e[j].next)
{
int v=e[j].v;
if(dist[v]>dist[u]+e[j].w)
{
dist[v]=dist[u]+e[j].w;
}
}
}
for(int i=1;i<=n;i++)
{
if(dist[i]==0x3f3f3f3f)
{
printf("2147483647 ");continue;
}
printf("%d ",dist[i]);
}
return 0;
}
最短路径SPFA
使用一种类似于宽搜的思路,将队首节点去除并扩展新结点,没有入队的将其入队,有两种优化SLF和LLL,理论上两种方法并用可将效率提高50%。
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int Inf=0x3f3f3f3f;
const int MAX_N=1e4+7;
const int MAX_M=5e5+7;
struct node{
int v,w,next;
}e[MAX_M*2];
int p[MAX_N];
int dist[MAX_M];
bool inq[MAX_N];
int n,m,s;
int eid;
void init()
{
mem(e,0);
mem(p,-1);
mem(inq,0);
mem(dist,0x3f);
eid=0;
}
void insert(int u,int v,int w)
{
e[eid].v=v;
e[eid].w=w;
e[eid].next=p[u];
p[u]=eid++;
}
int SPFA(int s)
{
queue<int> q;
q.push(s);
inq[s]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
inq[u]=0;
for(int j=p[u];~j;j=e[j].next)
{
int v=e[j].v;
if(dist[v]>dist[u]+e[j].w)
{
dist[v]=dist[u]+e[j].w;
if(!inq[v])
{
inq[v]=1;
q.push(v);
}
}
}
}
return 0;
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
init();
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
insert(u,v,w);
}
dist[s]=0;
SPFA(s);
for(int i=1;i<=n;i++)
{
if(dist[i]==0x3f3f3f3f)
{
printf("2147483647 ");continue;
}
printf("%d ",dist[i]);
}
return 0;
}
最近公共祖先Lca
给出一棵树和两个结点,求它们的公共祖先,由于数据量较大,所以一般用类似于稀疏表,倍增的思路完成,fa[u][i]代表从结点u开始向上找2^i个的结点的结点。
此处以洛谷3379为例
#include<iostream>
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_N=5e5+7;
const int MAX_M=5e5+7;
const int Lg2=25;
int n,m,s;
int eid;
struct node{
int v,next;
}e[MAX_M*2];
int d[MAX_N];
int p[MAX_N];
int fa[MAX_N*2][Lg2];
void init()
{
mem(e,0);
mem(p,-1);
mem(d,-1);
mem(fa,0);
d[s]=1;
eid=0;
}
void insert(int u,int v)
{
e[eid].v=v;
e[eid].next=p[u];
p[u]=eid++;
}
void insert2(int u,int v)
{
insert(u,v);
insert(v,u);
}
void dfs(int u)
{
for(int i=p[u];~i;i=e[i].next)
{
if(d[e[i].v]==-1)
{
int v=e[i].v;
d[v]=d[u]+1;
fa[v][0]=u;
dfs(v);
}
}
}
int Lca(int u,int v)
{
int i;
if(d[u]<d[v])
{
swap(u,v);
}
for(i=0;(1<<i)<=d[u];i++);
--i;
for(int j=i;j>=0;j--)
{
if(d[u]-(1<<j)>=d[v])
{
u=fa[u][j];
}
}
if(u==v) return u;
for(int j=i;j>=0;j--)
{
if(fa[u][j]!=fa[v][j])
{
u=fa[u][j];
v=fa[v][j];
}
}
return fa[u][0];
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
init();
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
insert2(u,v);
}
dfs(s);
for(int level=1;(1<<level)<=n;level++)
{
for(int i=1;i<=n;i++)
{
fa[i][level]=fa[fa[i][level-1]][level-1];
}
}
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
printf("%d\n",Lca(u,v));
}
return 0;
}
拓扑排序
完成一个任务就要首先完成其先决任务,问完成任务编号的序列,思想是建一个有向图,找到入度为0的点,删除,作为第一层,然后继续删除剩下的图。
此处以Poj2367为例
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
int n;
int ind[107];
struct node{
int v,next;
}e[20007];
int p[107];
bool vst[107];
queue<int> q;
int eid;
void init()
{
eid=0;
mem(ind,0);
mem(e,0);
mem(p,-1);
mem(vst,0);
}
void insert(int u,int v)
{
e[eid].v=v;
e[eid].next=p[u];
p[u]=eid++;
}
int main()
{
scanf("%d",&n);
init();
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
while(x)
{
ind[x]++;
insert(i,x);
scanf("%d",&x);
}
}
int ans=n;
while(ans)
{
for(int i=1;i<=n;i++)
{
if(!ind[i]&&!vst[i])
{
q.push(i);
vst[i]=1;
ans--;
}
}
while(!q.empty())
{
int u=q.front();
printf("%d ",u);
q.pop();
for(int i=p[u];~i;i=e[i].next)
{
int v=e[i].v;
ind[v]--;
}
}
}
return 0;
}
树的直径
求一棵树中最长的路径,用两次DFS或BFS,任选一个点搜索出离它最远的那个点,再从这个点开始,在搜索一次,找出的路径便是树的直径,此程序使用了DFS,BFS请读者自行完成
此处以Codevs1814为例
#include<iostream>
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_N=1e5+7;
const int MAX_M=1e5+7;
struct node{
int v,w,next;
}e[MAX_M*2];
int p[MAX_M];
bool vst[MAX_N];
int l[MAX_N],r[MAX_N];
int eid,pnt,start;
int n,ans;
void init()
{
mem(e,0);
mem(vst,0);
mem(p,-1);
eid=0;
}
void insert(int u,int v,int w)
{
e[eid].v=v;
e[eid].w=1;
e[eid].next=p[u];
p[u]=eid++;
}
void insert2(int u,int v,int w)
{
insert(u,v,w);
insert(v,u,w);
}
void dfs(int u,int cnt)
{
if(cnt>ans)
{
ans=cnt;
pnt=u;
}
for(int i=p[u];~i;i=e[i].next)
{
int v=e[i].v;
if(!vst[v])
{
vst[v]=1;
dfs(v,cnt+1);
vst[v]=0;
}
}
}
int main()
{
scanf("%d",&n);
init();
for(int i=1;i<=n;i++)
{
scanf("%d%d",&l[i],&r[i]);
if(l[i]&&r[i])
{
start=i;
}
if(l[i])
{
insert2(i,l[i],1);
}
if(r[i])
{
insert2(i,r[i],1);
}
}
ans=0;
dfs(start,0);
ans=0;
dfs(pnt,0);
printf("%d\n",ans);
return 0;
}
堆排序
一般的常用堆是小根堆和大根堆,也有比较强的斐波那契堆等,对于一个小根堆来说,一个结点的左右子树比自身都要大
此处以洛谷1177为例
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAX_N=1e5+7;
int n;
int len;
int heap[MAX_N];
void put(int x)
{
int son;
len++;
heap[len]=x;
son=len;
while(son!=1&&heap[son/2]>heap[son])
{
swap(heap[son],heap[son/2]);
son/=2;
}
}
int get()
{
int fa,son,tmp;
tmp=heap[1];
heap[1]=heap[len];
len--;
fa=1;
while((fa*2<=len)||(fa*2+1<=len))
{
if(fa*2+1>len||heap[fa*2]<heap[fa*2+1])
{
son=fa*2;
}
else
{
son=fa*2+1;
}
if(heap[fa]>heap[son])
{
swap(heap[fa],heap[son]);
fa=son;
}
else break;
}
return tmp;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
put(x);
}
for(int i=1;i<=n;i++)
{
printf("%d ",get());
}
printf("\n");
return 0;
}
并查集
判断连通块的一种经典算法,分为并和查两个部分,主要思路是找到两个待查询的结点,搜索其根节点。路径压缩是常用的优化。
此处以洛谷3367为例
#include<iostream>
#include<cstdio>
using namespace std;
const int MAX_N=1e4+7;
int n,m;
int fa[MAX_N];
int find(int x)
{
if(fa[x]==x)
{
return x;
}
return fa[x]=find(fa[x]);
}
void merge(int rootx,int rooty)
{
fa[rootx]=rooty;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
fa[i]=i;
}
for(int i=1;i<=m;i++)
{
int flag,x,y;
scanf("%d%d%d",&flag,&x,&y);
if(flag==1)
{
int rootx=find(x);
int rooty=find(y);
merge(rootx,rooty);
}
else
{
if(find(x)==find(y))
{
printf("Y\n");
}
else
{
printf("N\n");
}
}
}
return 0;
}
带权并查集
由不同并查集衍生成的一类数据结构,关键是在并与查的过程中求出每个点与根节点的关系,从而达到解决问题的效果,常与路径压缩连用。
此处以洛谷1196为例
#include<iostream>
#include<cmath>
using namespace std;
const int MAX_N=3e4+7;
int T;
int fa[MAX_N];
int dist[MAX_N];
int size[MAX_N];
int find(int x)
{
if(fa[x]==x)
{
return x;
}
int y=fa[x];
fa[x]=find(fa[x]);
dist[x]+=dist[y];
return fa[x];
}
void merge(int x,int y)
{
int rootx=find(x);
int rooty=find(y);
fa[rootx]=rooty;
dist[rootx]=size[rooty];
size[rooty]+=size[rootx];
}
int main()
{
cin>>T;
for(int i=1;i<=MAX_N;i++)
{
fa[i]=i;
dist[i]=0;
size[i]=1;
}
while(T--)
{
char flag;
int x,y;
cin>>flag>>x>>y;
if(flag=='C')
{
int rootx=find(x);
int rooty=find(y);
if(rootx!=rooty) cout<<-1<<endl;
else
{
cout<<abs(dist[x]-dist[y])-1<<endl;
}
}
else
{
merge(x,y);
}
}
return 0;
}
单调队列
是一种速度很快的数据结构,可以求连续区域内的最值,并保证其中元素的单调性,入队十分简单,出队有两种情况,一是队首已经被移出了所需区间,将队首删除,二是当前入队元素比队尾元素更优,将队尾删除。
此处以洛谷1886为例
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAX_N=1e6+7;
int n,k;
int head,tail;
int a[MAX_N];
int q[MAX_N],num[MAX_N],ans_max[MAX_N],ans_min[MAX_N];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
head=1;tail=0;
for(int i=1;i<=n;i++)
{
while(num[head]<i-k+1&&head<=tail) head++;
while(a[i]>q[tail]&&head<=tail) tail--;
q[++tail]=a[i];
num[tail]=i;
ans_max[i]=q[head];
}
memset(q,0,sizeof q);
head=1;tail=0;
for(int i=1;i<=n;i++)
{
while(num[head]<i-k+1&&head<=tail) head++;
while(a[i]<q[tail]&&head<=tail) tail--;
q[++tail]=a[i];
num[tail]=i;
ans_min[i]=q[head];
}
for(int i=k;i<=n;i++)
{
printf("%d ",ans_min[i]);
}
printf("\n");
for(int i=k;i<=n;i++)
{
printf("%d ",ans_max[i]);
}
return 0;
}
优先队列
和单调队列的功能比较类似,调用了C++了的STL,每次自动调整队首元素为当前最值,有入队和出队两种操作,还能调用当前队首,成员函数分别是push、pop、top。注意:考试时可能会在时间上卡STL(如上一题)。
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<queue>
using namespace std;
const int MAX_N=1e6+7;
int n,k;
int ans_Max[MAX_N],ans_Min[MAX_N];
int a[MAX_N];
struct cmp_Max
{
bool operator() (int x,int y)
{
return a[x]<a[y];
}
};
struct cmp_Min
{
bool operator() (int x,int y)
{
return a[x]>a[y];
}
};
priority_queue <int,vector<int>,cmp_Max> q_Max;
priority_queue <int,vector<int>,cmp_Min> q_Min;
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
if(i<=k)
{
q_Max.push(i);
q_Min.push(i);
}
}
int p=0;
ans_Max[++p]=a[q_Max.top()];
ans_Min[p]=a[q_Min.top()];
for(int i=k+1;i<=n;i++)
{
q_Max.push(i);
q_Min.push(i);
while(i-q_Max.top()>=k) q_Max.pop();
while(i-q_Min.top()>=k) q_Min.pop();
ans_Max[++p]=a[q_Max.top()];
ans_Min[p]=a[q_Min.top()];
}
for(int i=1;i<=n-k+1;i++)
{
printf("%d ",ans_Min[i]);
}
printf("\n");
for(int i=1;i<=n-k+1;i++)
{
printf("%d ",ans_Max[i]);
}
return 0;
}
ST表
也被称作稀疏表,以倍增作为主要思路,是解决静态RMQ类问题的绝佳利器,
此处以洛谷3865为例
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int MAX_N=1e5+7;
const int Lg2=20;
int n,m;
int st[2*MAX_N][Lg2];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&st[i][0]);
}
for(int level=1;(1<<level)<=n;level++)
{
for(int i=1;i<=n;i++)
{
st[i][level]=max(st[i][level-1],st[i+(1<<(level-1))][level-1]);
}
}
for(int i=1;i<=m;i++)
{
int l,r;
scanf("%d%d",&l,&r);
int len=floor(log(r-l+1)/log(2));
printf("%d\n",max(st[l][len],st[r-(1<<len)+1][len]));
}
return 0;
}
树状数组
解决动态RMQ问题的经典在线算法,其核心是利用位运算巧妙解决数组间的关系
此处以洛谷3374为例
#include<iostream>
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_N=5e5+7;
int c[MAX_N];
int n,m;
int lowbit(int x)
{
return x&(-x);
}
void modify(int x,int k)
{
for(;x<=n;x+=lowbit(x))
{
c[x]+=k;
}
}
int getsum(int x)
{
int sum=0;
for(;x;x-=lowbit(x))
{
sum+=c[x];
}
return sum;
}
int main()
{
scanf("%d%d",&n,&m);
mem(c,0);
for(int i=1;i<=n;i++)
{
int k;
scanf("%d",&k);
modify(i,k);
}
for(int i=1;i<=m;i++)
{
int flag,x,y;
scanf("%d%d%d",&flag,&x,&y);
if(flag==1)
{
modify(x,y);
}
else
{
printf("%d\n",getsum(y)-getsum(x-1));
}
}
return 0;
}
线段树单点修改+区间求和
和树状数组一样能解决动态RMQ,建树,树中记录线段的左端点右端点以及区间和等信息,这是线段树最基本的一种形式。
#include<iostream>
#include<cstdio>
using namespace std;
const int MAX_N=5e5+7;
int s[MAX_N*4];
int n,q;
void up(int p)
{
s[p]=s[2*p]+s[2*p+1];
}
void modify(int p,int l,int r,int x,int v)
{
if(l==r)
{
s[p]+=v;
return;
}
int mid=(l+r)/2;
if(x<=mid)
{
modify(p*2,l,mid,x,v);
}
else
{
modify(p*2+1,mid+1,r,x,v);
}
up(p);
}
int query(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y) return s[p];
int mid=(l+r)/2,res=0;
if(x<=mid) res+=query(p*2,l,mid,x,y);
if(y>mid) res+=query(p*2+1,mid+1,r,x,y);
return res;
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
{
int d;
scanf("%d",&d);
modify(1,1,n,i,d);
}
for(int i=1;i<=q;i++)
{
int d,x,y;
scanf("%d%d%d",&d,&x,&y);
if(d==1)
{
modify(1,1,n,x,y);
}
else
{
printf("%d\n",query(1,1,n,x,y));
}
}
return 0;
}
线段树懒惰标记
主要应用于区间修改和区间查询,功能较为强大,如果当前区间被所需区间包含,那么修改这部分的值,将这个地方置上懒惰标记,然后暂停往下更新,当要用到这部分以下的端点,再把该结点的懒惰标记下传(否则浪费时间),并消除这个点的懒惰标记
此处以Hdu1698为例
#include<iostream>
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_N=1e5+7;
int s[MAX_N*4],col[MAX_N*4];
int T,n,m,num=0;
void init()
{
mem(s,0);
mem(col,0);
}
void up(int p)
{
s[p]=s[p*2]+s[p*2+1];
}
void down(int p,int l,int r)
{
if(col[p])
{
int mid=(l+r)/2;
s[p*2]=col[p]*(mid-l+1);
s[p*2+1]=col[p]*(r-mid);
col[p*2]=col[p];
col[p*2+1]=col[p];
col[p]=0;
}
}
void modify(int p,int l,int r,int x,int y,int c)
{
if(x<=l&&r<=y)
{
s[p]=(r-l+1)*c;
col[p]=c;
return;
}
down(p,l,r);
int mid=(l+r)/2;
if(x<=mid)
{
modify(p*2,l,mid,x,y,c);
}
if(y>mid)
{
modify(p*2+1,mid+1,r,x,y,c);
}
up(p);
}
int query(int p,int l,int r,int x,int y)
{
int res=0;
if(x<=l&&r<=y)
{
return s[p];
}
int mid=(l+r)/2;
if(x<=mid)
{
res+=query(p*2,l,mid,x,y);
}
if(y>mid)
{
res+=query(p*2+1,mid+1,r,x,y);
}
up(p);
return res;
}
int main()
{
scanf("%d",&T);
while(T--)
{
num++;
scanf("%d",&n);
init();
modify(1,1,n,1,n,1);
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
modify(1,1,n,x,y,z);
}
printf("Case %d: The total value of the hook is %d.\n",num,query(1,1,n,1,n));
}
return 0;
}
莫队
很实用的一种离线算法,在一定条件下处理区间问有由很大用处,莫队算法将查询数据离线,以分块排序和指针移动为主要思路,减少时间上的损耗
此处以洛谷1972为例
注:该题加大了数据量,莫队无法通过全部测试数据。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAX_N=5e5+7;
const int MAX_M=5e5+7;
const int MAX_V=1e6+7;
struct node{
int l,r,num;
}q[MAX_M];
int n,m,ans;
int a[MAX_N];
int res[MAX_M],cnt[MAX_V];
int part;
bool cmp(node x,node y)
{
if(x.l/part==y.l/part)
{
return x.r<y.r;
}
return x.l<y.l;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&q[i].l,&q[i].r);
q[i].num=i;
}
part=floor(sqrt(n*1.0+0.5));
sort(q+1,q+m+1,cmp);
int l,r;
l=r=1;
cnt[a[1]]=1;
ans=1;
for(int i=1;i<=m;i++)
{
while(l<q[i].l)
{
cnt[a[l]]--;
if(!cnt[a[l]]) ans--;
l++;
}
while(l>q[i].l)
{
l--;
cnt[a[l]]++;
if(cnt[a[l]]==1) ans++;
}
while(r>q[i].r)
{
cnt[a[r]]--;
if(!cnt[a[r]]) ans--;
r--;
}
while(r<q[i].r)
{
r++;
cnt[a[r]]++;
if(cnt[a[r]]==1) ans++;
}
res[q[i].num]=ans;
}
for(int i=1;i<=m;i++)
{
printf("%d\n",res[i]);
}
return 0;
}
巴什博弈
较为简单的一类博弈,平衡态构造较为容易,根据石子的总数来确定先手或后手赢。
此处以Loj10241为例
#include<iostream>
#include<cstdio>
using namespace std;
int n,k;
int main()
{
scanf("%d%d",&n,&k);
if(n%(k+1)==0)
{
printf("2\n");
}
else
{
printf("1\n");
}
return 0;
}
尼姆博弈
经典的博弈问题,主要思路是将数字转换为二进制计算,可以使用亦或运算符计算,当先手可以将状态转为平衡态时,后手必败。
此处以洛谷2197为例
#include<iostream>
#include<cstdio>
using namespace std;
int T;
int main()
{
scanf("%d",&T);
while(T--)
{
int n;
scanf("%d",&n);
int ans=0;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
ans^=x;
}
if(!ans)
{
printf("No\n");
}
else
{
printf("Yes\n");
}
}
return 0;
}
斐波那契博弈
经典的博弈问题,当石子数为斐波那契数时,先手必败。
此处以Nyoj358为例
#include<iostream>
#include<cstdio>
#define LL long long
const int N=100;
LL n;
LL f[N];
int main()
{
f[1]=1;f[2]=2;
for(int i=3;i<=N;i++)
{
f[i]=f[i-1]+f[i-2];
}
while(~scanf("%lld",&n))
{
bool judge=1;
for(int i=1;i<=N;i++)
{
if(f[i]==n)
{
judge=0;
break;
}
}
if(!judge)
{
printf("No\n");
}
else
{
printf("Yes\n");
}
}
return 0;
}
威佐夫博弈
经典的博弈问题,重点是奇异态的寻找,在探究过程中可以找规律,最后能够获得奇异态的推导公式。
此处以Poj1067为例
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int x,y;
int main()
{
while(~scanf("%d%d",&x,&y))
{
if(x>y)
{
swap(x,y);
}
int k=y-x;
if(floor(k*(sqrt(5)+1)/2)==x)
{
printf("0\n");
}
else
{
printf("1\n");
}
}
return 0;
}
快速幂
快速幂+取模运算是用来求解大数据乘方的算法,利用a2b=(ab)2巧妙得出结果,取模运算防止不爆大数据。
此处以洛谷1226为例
#include<iostream>
#include<cstdio>
#define LL long long
using namespace std;
LL b,p,k;
LL pow(LL b,LL p)
{
if(p==0) return 1;
if(p%2)
{
LL x=pow(b,p/2)%k;
return (((x*x)%k)*b)%k;
}
else
{
LL x=pow(b,p/2)%k;
return (x*x)%k;
}
}
int main()
{
scanf("%lld%lld%lld",&b,&p,&k);
printf("%lld^%lld mod %lld=%lld",b,p,k,pow(b,p)%k);
return 0;
}
深度优先搜索
简称DFS,以搜索深度作为第一条件,被誉为骗分神器的搜索,在一定情况下可以展现自己优异的偏分性能,在加上合适的优化后效果更佳,从总体上说,搜索也有有利的一面。
此处以对一个有向图进行深度优先搜索为例
#include<iostream>
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_M=1e5+7;
const int MAX_N=1e5+7;
struct node{
int v,w,next;
}e[MAX_M];
int p[MAX_N];
bool vst[MAX_N];
int n,m,s,eid;
void init()
{
mem(e,0);
mem(vst,0);
mem(p,-1);
eid=0;
}
void insert(int u,int v,int w)
{
e[eid].v=v;
e[eid].w=w;
e[eid].next=p[u];
p[u]=eid++;
}
void dfs(int u)
{
printf("%d ",u);
for(int i=p[u];~i;i=e[i].next)
{
int v=e[i].v;
if(!vst[v])
{
vst[v]=1;
dfs(v);
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
init();
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
insert(u,v,w) ;
}
vst[s]=0;
dfs(s);
return 0;
}
宽度优先搜索
简称BFS,与深搜正好相对,从起点开始向各个方向扩展,以宽度为扩展任务,使用队列辅助完成,理解BFS的基本原理,因为单独考察的题目比较少,所以它的几种变形显得尤为重要。
此处以宽度优先搜索遍历图为例。
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int MAX_N=100;
const int MAX_M=10000;
struct edge{
int v,w,next;
}e[MAX_M];
int p[MAX_N],eid;
int n,m,s;
void init()
{
memset(p,-1,sizeof(p));
eid=0;
}
void insert(int u,int v,int w)
{
e[eid].v=v;
e[eid].w=w;
e[eid].next=p[u];
p[u]=eid++;
}
bool vst[MAX_N];
void bfs(int v)
{
memset(vst,false,sizeof(vst));
queue<int> q;
q.push(v);
vst[v]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
printf("%d ",u);
for(int i=p[u];~i;i=e[i].next)
{
if(!vst[e[i].v])
{
vst[e[i].v]=1;
q.push(e[i].v);
}
}
}
}
int main()
{
init();
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
insert(u,v,w);
}
bfs(s);
return 0;
}
双端队列宽度优先搜索
很明显宽搜无法处理权重不同的图,但或许边权只有0或1的图是个特例,善用宽度优先搜索的两个性质,两段性和单调性,可将扩展中边权为0的点插入队首,边权为1的点插入队尾,从而不破坏Bfs的两个性质。
此处以洛谷2243为例
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int cap=5e5+10;
const int L=507;
int dist[L][L];
char map[L][L];
int r,c;
int li,ri;
pair<int,int> queue[cap*2];
bool valid(int x,int y)
{
return (x>=0&&x<=r&&y>=0&&y<=c);
}
void que_add(int x,int y,int v)
{
if(dist[x][y]<0||v<dist[x][y])
{
dist[x][y]=v;
if(li==ri||v>dist[queue[li].first][queue[li].second])
{
queue[ri++]=make_pair(x,y);
}
else
queue[--li]=make_pair(x,y);
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&r,&c);
for(int i=0;i<r;i++)
{
scanf("%s",map[i]);
}
if((r+c)%2)
{
printf("NO SOLUTION\n");
continue;
}
li=ri=cap;
mem(queue,0);
mem(dist,-1);
dist[0][0]=0;
queue[ri++]=make_pair(0,0);
while(li!=ri)
{
int cx=queue[li].first,cy=queue[li].second;
++li;
if(valid(cx-1,cy-1))
{
if(map[cx-1][cy-1]=='\\') que_add(cx-1,cy-1,dist[cx][cy]);
else que_add(cx-1,cy-1,dist[cx][cy]+1);
}
if(valid(cx-1,cy+1))
{
if(map[cx-1][cy]=='/') que_add(cx-1,cy+1,dist[cx][cy]);
else que_add(cx-1,cy+1,dist[cx][cy]+1);
}
if(valid(cx+1,cy-1))
{
if(map[cx][cy-1]=='/') que_add(cx+1,cy-1,dist[cx][cy]);
else que_add(cx+1,cy-1,dist[cx][cy]+1);
}
if(valid(cx+1,cy+1))
{
if(map[cx][cy]=='\\') que_add(cx+1,cy+1,dist[cx][cy]);
else que_add(cx+1,cy+1,dist[cx][cy]+1);
}
}
printf("%d\n",dist[r][c]);
}
return 0;
}
双向宽度优先搜索
可以用于优化宽搜,从起点和终点两个方向交替搜索,可以节省很多时间,q[0][].x/.y代表从起点开始搜的路径上坐标,q[1][].x/.y代表从终点开始搜坐标,l[0],[1],r[0],[1]分别代表起点终点搜索的头尾指针,直到正向搜索和反向搜索有重合时,结束,在搜索中,已扩展节点数少的一边优先扩展。
此处以Poj1915为例
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_W=1e5+7;
const int MAX_L=307;
struct node{
int x,y;
}q[2][MAX_W];
int l[2],r[2];
int dist[2][MAX_L][MAX_L];
bool v[2][MAX_L][MAX_L];
int dx[8]={1,1,-1,-1,2,2,-2,-2};
int dy[8]={2,-2,2,-2,1,-1,1,-1};
int n,L,ans;
bool expand(int k)
{
int x,y,d;
x=q[k][l[k]].x;
y=q[k][l[k]].y;
d=dist[k][x][y];
for(int i=0;i<8;i++)
{
int tx,ty;
tx=x+dx[i];ty=y+dy[i];
if(tx>=0&&tx<L&&ty>=0&&ty<L&&!v[k][tx][ty])
{
v[k][tx][ty]=1;
r[k]++;
q[k][r[k]].x=tx;q[k][r[k]].y=ty;
dist[k][tx][ty]=d+1;
if(v[1-k][tx][ty])
{
ans=dist[k][tx][ty]+dist[1-k][tx][ty];
return 1;
}
}
}
return 0;
}
void Bfs()
{
v[0][q[0][1].x][q[0][1].y]=1;
v[1][q[1][1].x][q[1][1].y]=1;
l[0]=r[0]=1;l[1]=r[1]=1;
while(l[0]<=r[0]&&l[1]<=r[1])
{
if(r[0]-l[0]<r[1]-l[1])
{
if(expand(0)) return;
l[0]++;
}
else
{
if(expand(1)) return;
l[1]++;
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&L);
ans=0;
int sx,sy,ex,ey;
mem(q,0);mem(v,0);mem(dist,0);mem(l,0);mem(r,0);
scanf("%d%d%d%d",&sx,&sy,&ex,&ey);
q[0][1].x=sx;q[0][1].y=sy;
q[1][1].x=ex;q[1][1].y=ey;
if(q[0][1].x==q[1][1].x&&q[0][1].y==q[1][1].y)
{
printf("0\n");
continue;
}
Bfs();
printf("%d\n",ans);
}
return 0;
}
二分
在一个有序序列中查找给定的值,速度上优于顺序查找
此处以在一个递增序列中,查找给定值为例
#include<iostream>
#include<cstdio>
using namespace std;
const int MAX_N=1e5+7;
int n,k;
int a[MAX_N];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
int l=1,r=n;
while(l<=r)
{
int mid=(l+r)/2;
if(a[mid]==k)
{
printf("%d\n",mid);
return 0;
}
if(a[mid]>k) r=mid-1;
else l=mid+1;
}
return 0;
}
三分
对象主要是单峰区间,将所需区间划分为三段,可以证明最优点和好点在坏点的同侧,依据此方法缩小区间,注意三分判断时的精确度应该是保留小数位数的下一位
此处以洛谷3382为例
#include<iostream>
#include<iomanip>
#include<cstring>
#include<cstdio>
using namespace std;
int n;
double l,r;
double a[15];
double cal(double x)
{
double sum=0;
double pow=1;
for(int i=n+1;i>=1;i--)
{
sum+=pow*a[i];
pow*=x;
}
return sum;
}
int main()
{
scanf("%d%lf%lf",&n,&l,&r);
for(int i=1;i<=n+1;i++)
{
scanf("%lf",&a[i]);
}
while(r-l>1e-6)
{
double lmid,rmid;
lmid=l+(r-l)/3;
rmid=r-(r-l)/3;
if(cal(lmid)>cal(rmid)) r=rmid;
else l=lmid;
}
printf("%.5lf\n",l);
return 0;
}
哈希优化
是一种优化,如处理二维数据时,原来冗余的部分在于判重,而将各种类型的数据转换为数字,所以只要构造一个哈希函数和标记数组就可以了,常见方法有二进制状态压缩,直接取余以及平方取中。
此处以洛谷4289为例,典型宽搜+二进制压缩优化
#include<iostream>
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int n=4;
const int MAX_L=1e6+7;
const int MAX_N=1e5+7;
const int dx[4]={0,1,0,-1};
const int dy[4]={-1,0,1,0};
struct form{
int a[n][n];
}q[MAX_L],start,goal;
int num[n*n+3];
int cnt[MAX_L];
bool flag[MAX_N];
char map[n+3][n+3];
int s,g;
void init()
{
mem(num,0);
mem(q,0);
mem(flag,0);
mem(cnt,0);
}
bool valid(int x,int y)
{
return(x>=0&&x<n&&y>=0&&y<n);
}
int turn(form x)
{
int m=0,hash=0,pow=1;
for(int i=n-1;i>=0;i--)
{
for(int j=n-1;j>=0;j--)
{
num[++m]=x.a[i][j];
}
}
for(int i=1;i<=m;i++)
{
hash+=pow*num[i];
pow*=2;
}
return hash;
}
void Bfs()
{
int head,tail;
q[1]=start;
head=tail=1;
while(head<=tail)
{
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
for(int k=0;k<4;k++)
{
int ti=i+dx[k];
int tj=j+dy[k];
if(valid(ti,tj)&&q[head].a[i][j])
{
form tmp=q[head];
int temp_val,t;
temp_val=tmp.a[i][j];
tmp.a[i][j]=tmp.a[ti][tj];
tmp.a[ti][tj]=temp_val;
t=turn(tmp);
if(!flag[t])
{
//printf("%d\n",t);
flag[t]=1;
q[++tail]=tmp;
cnt[tail]=cnt[head]+1;
if(t==g)
{
printf("%d\n",cnt[tail]);
return;
}
}
}
}
}
}
head++;
}
}
int main()
{
for(int i=0;i<n;i++)
{
scanf("%s",map[i]);
for(int j=0;j<n;j++)
{
start.a[i][j]=map[i][j]-48;
}
}
for(int i=0;i<n;i++)
{
scanf("%s",map[i]);
for(int j=0;j<n;j++)
{
goal.a[i][j]=map[i][j]-48;
}
}
s=turn(start);
g=turn(goal);
if(s==g)
{
printf("0\n");
return 0;
}
Bfs();
return 0;
}
01背包
背包,动态规划的基础问题,其中以01背包最为容易,重点掌握二维到一维数组的空间压缩过程,以及转移方程的思想。
此处以洛谷1048为例
#include<iostream>
#include<cstdio>
using namespace std;
const int MAX_T=1007;
const int MAX_M=107;
int t,m;
int w[MAX_M],c[MAX_M];
int f[MAX_T];
int main()
{
scanf("%d%d",&t,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&w[i],&c[i]);
}
for(int i=1;i<=m;i++)
{
for(int j=t;j>=w[i];j--)
{
f[j]=max(f[j],f[j-w[i]]+c[i]);
}
}
printf("%d\n",f[t]);
return 0;
}
完全背包
完全背包相对于01背包来说唯一的区别在于每种物品的个数没有限制,而且程序只需在01背包的基础上稍微改动一下。
此处以求n个物品完全背包的最大价值为例
#include<iostream>
#include<cstdio>
using namespace std;
const int MAX_T=1007;
const int MAX_M=107;
int t,m;
int w[MAX_M],c[MAX_M];
int f[MAX_T];
int main()
{
scanf("%d%d",&t,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&w[i],&c[i]);
}
for(int i=1;i<=m;i++)
{
for(int j=w[i];j<=t;j++)
{
f[j]=max(f[j],f[j-w[i]]+c[i]);
}
}
printf("%d\n",f[t]);
return 0;
}
多重背包优化
多重背包有朴素做法,即将其拆分为01背包,但由于拆分之后数据过大,时间空间都有风险,优化做法是将一个种类的个数拆分成二的幂次方,然后再用01背包计算,因为二的幂次方可以表示任何正整数
此处以将多重背包分为价值相等的两组为例
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int MAX_N=6*2e5+7;
const int MAX_T=6*2e5+7;
int p[10];
int a[MAX_N];
bool f[MAX_T];
int n=0,s=0;
void init()
{
mem(a,0);
mem(p,0);
mem(f,0);
}
void pack()
{
f[0]=true;
for(int i=1;i<=n;i++)
{
for(int j=s;j>=0;j--)
{
if(f[j]&&j+a[i]<=s)
{
f[j+a[i]]=true;
}
}
}
}
int main()
{
init();
for(int i=1;i<=6;i++)
{
scanf("%d",&p[i]);
s+=p[i]*i;
if(p[i]==0) continue;
int m=floor(1.0*log(p[i])/log(2));
int cnt=1;
for(int j=1;j<=m;j++)
{
n++;
a[n]=i*cnt;
cnt*=2;
}
n++;
a[n]=(p[i]-cnt/2)*i;
}
pack();
if(s%2==0&&f[s/2])
{
printf("Can be divided.\n");
}
else
{
printf("Can't be divided.\n");
}
return 0;
}
一维差分
基于前缀和的一种思路,主要能够解决区间修改加单点查询的问题,可以和树状数组,线段树等联合使用
此处给出一维差分的核心程序段
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
s[y+1]-=z;
s[x]+=z;
}
二维差分
思路类比一维差分,使用一个差分数组记录内容
此处给出二维差分核心程序段(以d数组作为差分数组,(a,b)(c,d)分别为起终点坐标)
for(int i=1;i<=m;i++)
{
scanf("%d%d%d%d",&a,&b,&c,&d);
change(a,b,1);
change(a,d+1,-1);
change(c+1,b,-1);
change(c+1,d+1,-1);
}
树上点差分
没什么好说的,注意特殊处理,要处理到Lca的父节点
此处给出树上点差分的核心程序段
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&d);
w[x]+=d;
w[y]+=d;
w[lca(x,y)]-=d;
w[fa[lca(x,y)][0]]-=d;
}
树上边差分
边差分与点差分稍微有些不一样,更加方便,实质上也是点储存边信息
此处给出树上边差分的核心程序段
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&d);
w[x]+=d;
w[y]-=d;
w[lca(x,y)]-=2*d;
}
裴蜀定理
n个整数之间的裴蜀定理
设a1,a2,a3…an为n个整数,d是它们的最大公约数,那么存在整数x1…xn使得x1a1+x2a2+…xn*an=d
此处以洛谷4549为例
#include<bits/stdc++.h>
using namespace std;
int n,x,ans;
int gcd(int x,int y)
{
int r=x%y;
while(r)
{
x=y,y=r,r=x%y;
}
return y;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
if(!x) continue;
if(x<0) x=-x;
if(i==1)
{
ans=x;
}
else
{
ans=gcd(ans,x);
}
}
printf("%d\n",ans);
return 0;
}