搜索选讲、分块初步、莫队简介
Treap_Kongzs · · 算法·理论
省流:本篇专供冲击NOIP一等的人使用,坐标HN
博客同步
1:搜索选讲
因为纯靠搜索能AC的题目少得离谱,所以初学阶段,我们不讲很多高深的搜索,以下四个算法应该足以应付我们的目标了。
1.1 深度优先搜索DFS
深度优先搜索指的是用递归完成的
遍历枚举,遍历请转移至图论
其思想很简单,就是一条路走到~~
WA~~
DFS枚举一般使用递归实现,至于图论里的非递归+栈,到时候再讲
它有一个小优化,就是当枚举的位置/元素不符合要求的时候,可以通过撤销操作往回跑,叫恢复现场也可以。其实,它的真正名字叫
回溯怎么实现呢?你赋了哪些值、改变了哪些变量,改回去不就是了
就这样,你已经了解了这个相当简洁的算法
试一试!
习题1.1.1
P1219 八皇后
明明是n皇后
对于这道题,我们可以学习图的行-列-对角线标记法
即对于行、列、左/右对角线,我们分别开一个数组标记
对于对角线存储的原理,我们观察可得:
但右对角线的定值可能
此时定值为
故我们加一个
然后是核心的dfs部分,
我们将dfs的参数设为行数,及对每一行进行dfs,
一旦这个格子可以放,就将这个格子所在的行、列、左/右对角线全部打上标记(示例代码记为1)
显然回溯时全部定为0就可以了
如何判断可不可以放呢?当且仅当格子所在的行、列、左/右对角线都未标记时,这里是可以放的。
然后输出就可以了
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=30;
//int line[maxn]; legacy
int column[maxn];
int lefts[maxn];
int rights[maxn];
int way[maxn];//三种方案
int ans=0;//最终输出的次数
int cnt=1;//为了让打印答案更直观,添加一个辅助变量
void printarr(int len)
{
if(cnt<=3)
{
for(int i=1;i<=len;i++)
{
cout<<way[i];
if(i<len)cout<<" ";
}
cout<<endl;
}
ans++;
cnt++;
}
void dfs(int k,int n)//这里的k是列数,如果行和列都枚一遍,那就循环了,还递归什么
{
if(k>n)//行数枚完了
{
printarr(n);
return;
}
for(int i=1;i<=n;i++)
{
if(column[i]==0&&lefts[i+k]==0&&rights[i-k+n]==0)
{
way[k]=i;//别忘了保存答案,第k行的答案(列数)是i
column[i]=1;
lefts[i+k]=1;
rights[i-k+n]=1;
dfs(k+1,n);//下一行,启动!
//if 要不得,回溯,撤!
column[i]=0;
lefts[i+k]=0;
rights[i-k+n]=0;
}
}
}
int main()
{
int n=0;
cin>>n;
dfs(1,n);
cout<<ans;
return 0;
}
习题1.1.2
P1135 奇怪的电梯
我一看,哦,很简单的题目,dfs的二叉搜索就够了
递归时先判断合不合法,然后再调用
然后你就收获了
#include<bits/stdc++.h>
using namespace std;
const int maxn=205;
int arr[maxn];
int cnt=0;
void dfs(int s,int t,int n)
{
if(s==t)
{
return;
}
else
{
if(s+arr[s]<=n)
{
cnt++;
dfs(s+arr[s],t,n);
}
else if(s-arr[s]>=1)
{
cnt++;
dfs(s-arr[s],t,n);
}
else
{
cout<<"No roads"<<endl;
return;
}
}
}
int main()
{
int n=0,a=0,b=0;
cin>>n>>a>>b;
for(int i=1;i<=n;i++)
{
cin>>arr[i];
}
dfs(a,b,n);
cout<<cnt;
return 0;
}
值得一提的是,dfs不做任何优化的情况下时间复杂度为
1.2 广度优先搜索BFS
这个算法基本是为图论服务的,网上单独对它进行讲述的资料很少,有的甚至只有两行,点名批评某wiki
但以后的图论最短路算法都是从它的基础上衍生出去的,写法看上去还差不多,所以我们讲讲。
广度优先搜索基本上以循环实现,用队列来储存所有与当前元素相邻的元素,再一个个从队列中取出,以它为当前元素,再入队相邻元素,如此往复。
值得一提的是,虽然
裸的不加优化的bfs似乎也是O(2^n) ,但在一些具体题目中,它很可能达到O(n+m) 的优秀时间复杂度,其中n 为元素数,m 为元素之间的关系数
习题1.2.1
P1135 奇怪的电梯 again
好,我们用bfs来处理一下这个题目
我们使用
其余的写法如上文所述。
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=205;
int arr[maxn];
int vis[maxn];
int dis[maxn];
int cnt=0;
void bfs(int s,int t,int n)
{
queue<int> q;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
vis[s]=1;
dis[s]=0;
if(s+arr[s]<=n&&vis[s+arr[s]]==0)
{
q.push(s+arr[s]);
vis[s+arr[s]]=1;
dis[s+arr[s]]=min(dis[s+arr[s]],dis[s]+1);
}
if(s-arr[s]>=1&&vis[s-arr[s]]==0)
{
q.push(s-arr[s]);
vis[s-arr[s]]=1;
dis[s-arr[s]]=min(dis[s-arr[s]],dis[s]+1);
}
while(!q.empty())
{
int u=q.front();
q.pop();
if(u==t)return;
if(u+arr[u]<=n&&vis[u+arr[u]]==0)
{
q.push(u+arr[u]);
vis[u+arr[u]]=1;
dis[u+arr[u]]=min(dis[u+arr[u]],dis[u]+1);
}
if(u-arr[u]>=1&&vis[u-arr[u]]==0)
{
q.push(u-arr[u]);
vis[u-arr[u]]=1;
dis[u-arr[u]]=min(dis[u-arr[u]],dis[u]+1);
}
}
}
int main()
{
int n=0,a=0,b=0;
cin>>n>>a>>b;
for(int i=1;i<=n;i++)
{
cin>>arr[i];
}
bfs(a,b,n);
if(dis[b]==0x3f3f3f3f)
{
cout<<-1;
return 0;
}
cout<<dis[b];
return 0;
}
其实这题的搜索更适合用图论建模的思想,把能到的楼层都连上边,然后跑图论版的
习题1.2.2
P1443 马的遍历
这个就是bfs的板子题了,一点技术含量都没有
我们直接把马的八个运动方向分解到棋盘的一大坨的bfs
本题bfs的入队条件是运动到目的地后不越界,即
为了避免一个格子被覆盖多个路径,我们使用二维数组
其余照常bfs即可,注意本题输出数组好像是用水平制表符分隔的,不知道空格会不会错。
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=405;
int dis[maxn][maxn];
int vis[maxn][maxn];
int xs[]={0,-1,-2,-2,-1,1,2,2,1};
int ys[]={0,2,1,-1,-2,-2,-1,1,2};
struct xy
{
int x;
int y;
};
void bfs(int n,int m,int x,int y)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
queue<xy> q;
q.push({x,y});
dis[x][y]=0;
vis[x][y]=1;
while(!q.empty())
{
xy u=q.front();
q.pop();
for(int i=1;i<=8;i++)
{
if(u.x+xs[i]<=n&&u.x+xs[i]>=1&&u.y+ys[i]<=m&&u.y+ys[i]>=1&&vis[u.x+xs[i]][u.y+ys[i]]==0)
{
dis[u.x+xs[i]][u.y+ys[i]]=dis[u.x][u.y]+1;
vis[u.x+xs[i]][u.y+ys[i]]=1;
q.push({u.x+xs[i],u.y+ys[i]});
}
}
}
}
int main()
{
int n=0,m=0,x=0,y=0;
cin>>n>>m>>x>>y;
bfs(n,m,x,y);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(dis[i][j]==0x3f3f3f3f)cout<<-1;
else cout<<dis[i][j];
if(j<m)cout<<'\t';
}
if(i<n)cout<<endl;
}
return 0;
}
1.3 双向搜索/meeting in the middle
很快啊,这里已经只剩
这个译名真的很乱啊,而且这两个真不是一个算法!
双向搜索就是同时从起点
s 和终点t 同时dfs或bfs,那么问题有可行解的充要条件是两条搜索路径能够相遇。
感觉写起来的话bfs实现比较方便,dfs可能头绪会有点乱。
meeting in the middle 常译作“折半搜索”,适用于以下情况:
O(n^2)<<O(数据规模)<<O(2^n)
即数据规模较小,但不至于直接dfs或bfs
其核心思想是将搜索路径分成两部分进行,最终使用时再合并。
分治了属于是
非常抽象是吧,看两道题吧
习题1.3.1
P4799 世界冰球锦标赛
经典meeting in the middle,这里最多有40场比赛,暴力枚举为
所以显然不能朴素dfs。而所谓“背包”算法也无法
事实上,折半搜索常用于
我们考虑将比赛分成两半,分开dfs求出可行方案数,并分别存在
分别进行两次dfs后,我们就应该考虑合并答案,那如何合并呢?
首先,我们必须让一个数组有序(这里选择
然后对于
这里对于
a 和b 稍作解释。a 和b 中的每一个元素代表一种可行的方案的钱数(sum ),比如样例中我们将1、2 和3、4、5 分开,对于前组,很明显有两种方案:不买和买第一场,那么,我们就往a 里pushback 两个答案,很明显,a 和b 组合时,答案遵循乘法原理,所以如此合并。
这真是道麻烦至极的
它是从
千万要认真吸收,吃透
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=50;
#define ll long long
ll price[maxn];//每场价钱
vector<ll> a;//前半段的搜索结果
vector<ll> b;//后半段的搜索结果
ll ans;//最终答案
void dfs(ll l,ll r,ll sum,vector<ll> &q,ll m)
{
if(sum>m)return;//买不起了,撤!
if(l>r)//到达目的地了,撤!
{
q.push_back(sum);//把答案扔进去
return;
}
dfs(l+1,r,sum+price[l],q,m);//这一场买了,处理下一场比赛
dfs(l+1,r,sum,q,m);//这一场没买,处理下一场比赛
}
int main()
{
ll n=0,m=0;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>price[i];
}
ll mid=n/2;//折半边界
dfs(1,mid,0,a,m);//前半段的搜索
dfs(mid+1,n,0,b,m);//后半段的搜索
sort(a.begin(),a.end());//我们选择使前半段有序
for(int i=0;i<b.size();i++)
{
ans+=upper_bound(a.begin(),a.end(),m-b[i])-a.begin();//对于后半段结果中的每一个,它对ans的贡献为前半段中与它相加小于m的数个数
}
cout<<ans;
return 0;
}
习题1.3.2
P1379 八数码难题
显然每个九位数的数列都是个状态,我们只要简单地写个单向bfs就可以了。
AC Code:
#include<bits/stdc++.h>
using namespace std;
int matrix[4][4];//模拟的地图
int xs[]={0,0,0,-1,1};//x方向的移动的常量数组
int ys[]={0,1,-1,0,0};//y方向的移动的常量数组
int res=123804765;//目的状态
int sx=0,sy=0;//0在地图中的位置
queue<int> q;//bfs队列
map<int,int> ans;//每个状态(一个九位数)的答案
void inttoma(int n)//数列转矩阵
{
for(int i=3;i>=1;i--)
{
for(int j=3;j>=1;j--)
{
matrix[i][j]=n%10;
n/=10;
if(matrix[i][j]==0)
{
sx=i;
sy=j;
}
}
}
}
int matoint()//矩阵转数列
{
int res=0;
int cnt=1;
for(int i=3;i>=1;i--)
{
for(int j=3;j>=1;j--)
{
res+=matrix[i][j]*cnt;
cnt*=10;
}
}
return res;
}
void dbfs(int s)//双向搜索
{
q.push(s);//起点入队
ans[s]=0;//起点距离为0
while(!q.empty())
{
int u=q.front();
q.pop();
if(u==res)break;//到达目的状态直接退出
inttoma(u);//数字转矩阵
for(int i=1;i<=4;i++)
{
int ux=sx+xs[i];//尝试挪动x
int uy=sy+ys[i];//尝试挪动y
int n=0;
if(ux<1||ux>3||uy<1||uy>3)continue;//越界就不动
swap(matrix[ux][uy],matrix[sx][sy]);//尝试挪动
n=matoint();//挪动结果转化成数列
if(!ans.count(n))//在这里,ans[n]==0可以起到vis[n]==0的作用,所以不需要额外的vis数组
{
ans[n]=ans[u]+1;//更新距离
q.push(n);//入队吧
}
swap(matrix[ux][uy],matrix[sx][sy]);//恢复现场,以便下次挪动
}
}
}
int main()
{
int s=0;
cin>>s;
dbfs(s);
cout<<ans[res];//res的距离
return 0;
}
但很明显它出在了双向bfs里面,事实上,这是我能找到的双向bfs的几乎唯一一道例题。还有一道是
现在让我们思考本题的双向bfs写法……要不我留作思考题吧:
鉴于维护两个bfs队列较为复杂,我们考虑还是只维护一个队列
我们在
我们先取队头
然后,我们将
-
u已访问,且与buf同向:vis[u]==vis[buf] -
u已访问,且与buf反向:vis[u]+vis[buf]==3 -
u未访问
对于Case 1,我们搜索下去完全没有意义了(因为有可能继续搜到同向访问过的点),所以恢复现场(方便下次移动),然后直接跳过其他情况。
对于Case 2,显然再走一步,两条搜索路径就会相交,此时起点和终点就有了一条通路,易证此时是符合题意的答案。
对于Case 3,是缺省答案,,既然
另外,如果开始就碰到了终点,记得开头就特判,不然
AC Code:
#include<bits/stdc++.h>
using namespace std;
int matrix[4][4];//模拟的地图
int xs[]={0,0,0,-1,1};//x方向的移动的常量数组
int ys[]={0,1,-1,0,0};//y方向的移动的常量数组
int res=123804765;//目的状态
int sx=0,sy=0;//0在地图中的位置
queue<int> q;//bfs队列
map<int,int> ans;//每个状态(一个九位数)的答案
map<int,int> vis;//标记数组,正向碰到了打1,逆向碰到了打2
void inttoma(int n)//数列转矩阵
{
for(int i=3;i>=1;i--)
{
for(int j=3;j>=1;j--)
{
matrix[i][j]=n%10;
n/=10;
if(matrix[i][j]==0)
{
sx=i;
sy=j;
}
}
}
}
int matoint()//矩阵转数列
{
int res=0;
int cnt=1;
for(int i=3;i>=1;i--)
{
for(int j=3;j>=1;j--)
{
res+=matrix[i][j]*cnt;
cnt*=10;
}
}
return res;
}
void dbfs(int s)//双向搜索
{
if(s==res)//很快啊,直接就到达目的地了
{
cout<<0;
return;
}
q.push(s);//起点入队
q.push(res);//终点入队
ans[s]=0;//起点距离为0
ans[res]=0;//终点距离也为0
vis[s]=1;//起点方向搜索打1标记
vis[res]=2;//终点方向搜索打2标记
while(!q.empty())
{
int u=q.front();
int buf=u;//存一份副本,buf是“上一步操作”
q.pop();
inttoma(u);//数字转矩阵
for(int i=1;i<=4;i++)
{
int ux=sx+xs[i];//尝试挪动x
int uy=sy+ys[i];//尝试挪动y
if(ux<1||ux>3||uy<1||uy>3)continue;//越界就不动
swap(matrix[ux][uy],matrix[sx][sy]);//尝试挪动
u=matoint();//挪动结果转化成数列
if(vis[u]==vis[buf])//如果u和buf已经是一个方向搜索到的,那就算了
{
swap(matrix[ux][uy],matrix[sx][sy]);//挪回来
continue;//下一位!!
}
else if(vis[u]+vis[buf]==3)//因为一个打了1标记,一个打了2标记,如果它们相加为3,下一步必然相遇,再加1就可以输出了
{
cout<<ans[u]+ans[buf]+1;//因为二者是相反方向搜索过来的,所以二者到各自起始节点的距离相加,再加最后交融的1步,就是起点到终点的路径
return;
}
else
{
ans[u]=ans[buf]+1;//默认情况,u是新开发地带,buf已被访问,自然加1
vis[u]=vis[buf];//保证u和buf是一个方向搜索的
q.push(u);//vis都打了,入队吧
swap(matrix[ux][uy],matrix[sx][sy]);//恢复现场,以便下次挪动
}
}
}
}
int main()
{
int s=0;
cin>>s;
dbfs(s);
return 0;
}
这道题同时是单向bfs和双向bfs的经典例题,且数字/字符串←→二维数组的思想是状态压缩类型动态规划的基础,一定要掌握牢实。
关于双队列的
2.分块初步
网上面将分块称之为“优雅的暴力”优雅个鬼
常用来解决
10^5 这一级数据规模的区间查找与修改问题,所以是线段树和树套树的同类算法
分块码量跟线段树差不多,其实思维也不比线段树容易理解多少,但是奇葩的事实是,分块作为平均
有一个重要前提: 分块的具体操作(查询题目需要的数据)必须是
O(1) 的,如果不是,莫队可以二次离线,分块暂时没辙。
分块就三个操作,初始化、区间修改、区间查询
初始化:我们一般将块的大小设计为
最后,如果有需要,将每个块的内部进行排序。
我们再设原数据储存在
区间修改:我们将待修改区间
-
[x,r[belong[x]] -
[r[belong[x]+1,l[belong[y]]-1 -
l[belong[y]],y]
对于Case 1&&3,我们直接暴力修改即可。
对于Case 2,我们显然可以偷懒,因为这是整块整块的区间,显然设个梦回线段树
在每种修改后,你都需要将
区间查询:类似的,我们可以将区间
设询问数
分块将大小设为
习题2.1.1
P2801 教主的魔法
窃以为这才是真的板子题
请留意讨论区提示,不要尝试使用线段树通过此题
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int arr[maxn];
int block=0;
int tot=0;
int l[maxn];
int r[maxn];
int belong[maxn];
int lazy[maxn];
int cpy[maxn];
void init(int n)
{
block=(int)sqrt(n);
tot=n/block;
if(n%block!=0)tot++;
for(int i=1;i<=tot;i++)
{
l[i]=(i-1)*block+1;
r[i]=i*block;
}
r[tot]=n;
for(int i=1;i<=n;i++)
{
belong[i]=(i-1)/block+1;
}
for(int i=1;i<=tot;i++)
{
sort(cpy+l[i],cpy+r[i]+1);
}
}
void add(int x,int y,int k)
{
if(belong[x]==belong[y])
{
for(int i=x;i<=y;i++)
{
arr[i]+=k;
}
for(int i=l[belong[x]];i<=r[belong[y]];i++)
{
cpy[i]=arr[i];
}
sort(cpy+l[belong[x]],cpy+r[belong[x]]+1);
return;
}
else
{
for(int i=x;i<=r[belong[x]];i++)
{
arr[i]+=k;
}
for(int i=l[belong[x]];i<=r[belong[x]];i++)
{
cpy[i]=arr[i];
}
sort(cpy+l[belong[x]],cpy+r[belong[x]]+1);
for(int i=l[belong[y]];i<=y;i++)
{
arr[i]+=k;
}
for(int i=l[belong[y]];i<=r[belong[y]];i++)
{
cpy[i]=arr[i];
}
sort(cpy+l[belong[y]],cpy+r[belong[y]]+1);
for(int i=belong[x]+1;i<=belong[y]-1;i++)
{
lazy[i]+=k;
}
}
}
void query(int x,int y,int k)
{
int ans=0;
if(belong[x]==belong[y])
{
for(int i=x;i<=y;i++)
{
if(lazy[belong[i]]+arr[i]>=k)ans++;
}
cout<<ans<<endl;
return;
}
for(int i=x;i<=r[belong[x]];i++)
{
if(lazy[belong[x]]+arr[i]>=k)ans++;
}
for(int i=l[belong[y]];i<=y;i++)
{
if(lazy[belong[y]]+arr[i]>=k)ans++;
}
for(int i=belong[x]+1;i<=belong[y]-1;i++)
{
int ll=l[i],rr=r[i],mid=0,res=0;
while(ll<=rr)
{
mid=(ll+rr)/2;
if(cpy[mid]+lazy[i]>=k)
{
rr=mid-1;
res=r[i]-mid+1;
}
else
{
ll=mid+1;
}
}
ans+=res;
}
cout<<ans<<endl;
}
int main()
{
int n=0,q=0;
cin>>n>>q;
for(int i=1;i<=n;i++)
{
cin>>arr[i];
}
memcpy(cpy,arr,sizeof(arr));
init(n);
char op;
int a,b,c;
for(int i=1;i<=q;i++)
{
cin>>op>>a>>b>>c;
if(op=='M')add(a,b,c);
if(op=='A')query2(a,b,c);
}
return 0;
}
对于分块预处理数组的构建,我觉得还是很难的,要多练一下。
分块的模板好打的锂谱,但有很多时候,你会卡在不知道维护什么数组上
但是那种题我也不会做,所以再来做道简单题吧
习题2.1.2
P2357 守墓人
模板题
但是呢,这题有所提示,输入不超过64位整数
十年OI一场空,不开
longlong 见祖宗
rp++
而且这题十分玄学,你甚至可以不初始化,直接就处理询问,只要
你可以观察到,我们在读入每个数后立马处理区间和,这样可以在卡常小trick get
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+7;
#define int long long
int arr[maxn];
int l[maxn];
int r[maxn];
int belong[maxn];
int cntb[maxn];
int lazy[maxn];
int block=0;
int tot=0;
void init(int n)
{
block=sqrt(n);
tot=n/block;
if(n%block!=0)tot++;
for(int i=1;i<=tot;i++)
{
l[i]=(i-1)*block+1;
r[i]=i*block;
}
r[tot]=n;
for(int i=1;i<=n;i++)
{
belong[i]=(i-1)/block+1;
}
}
void update(int x,int y,int k)
{
if(belong[x]==belong[y])
{
for(int i=x;i<=y;i++)
{
arr[i]+=k;
cntb[belong[i]]+=k;
}
return;
}
for(int i=x;i<=r[belong[x]];i++)
{
arr[i]+=k;
cntb[belong[i]]+=k;
}
for(int i=l[belong[y]];i<=y;i++)
{
arr[i]+=k;
cntb[belong[i]]+=k;
}
for(int i=belong[x]+1;i<=belong[y]-1;i++)
{
lazy[i]+=k;
cntb[i]+=k*(r[i]-l[i]+1);
}
}
int query(int x,int y)
{
int res=0;
if(belong[x]==belong[y])
{
for(int i=x;i<=y;i++)
{
res+=lazy[belong[i]]+arr[i];
}
return res;
}
for(int i=x;i<=r[belong[x]];i++)
{
res+=lazy[belong[i]]+arr[i];
}
for(int i=l[belong[y]];i<=y;i++)
{
res+=lazy[belong[i]]+arr[i];
}
for(int i=belong[x]+1;i<=belong[y]-1;i++)
{
res+=cntb[i];
}
return res;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n=0,m=0,op=0,x=0,y=0,k=0;
cin>>n>>m;
init(n);
for(int i=1;i<=n;i++)
{
cin>>arr[i];
cntb[belong[i]]+=arr[i];
}
for(int i=1;i<=m;i++)
{
cin>>op;
if(op==1)
{
cin>>x>>y>>k;
update(x,y,k);
}
else if(op==2)
{
cin>>k;
update(1,1,k);
}
else if(op==3)
{
cin>>k;
update(1,1,-k);
}
else if(op==4)
{
cin>>x>>y;
cout<<query(x,y);
if(i<m)cout<<endl;
}
else if(op==5)
{
cout<<query(1,1);
if(i<m)cout<<endl;
}
}
return 0;
}
但是我们不能将思维停留在这种水简单题上面,接下来来看一道之前说过的,怎么建立数组分块的题目。
习题2.1.3
P4135 作诗
这道题真的不是分块模板题!就算是,也有点难度,起码对你的前缀和思想考查要求很高。
一个很麻烦的事情是本题要维护的信息(询问区间内出现次数为偶数的数的个数)并不满足区间可加性,举个例子:
至于本题,对于一个长度为
10 的原数组,我们查询[4,9] 的答案,按\sqrt n 分块的话,是两个整区间[4,6] 和[7,9] ,没有散块,最简单的情况。假如在
[4,6] 里,3 出现了1 次,在[7,9] 里则出现了3 次,合到一起就是4 次,对答案产生了贡献,加进去是吧?模板分块下,你根本预判不到这种情况,除非愿意退化为暴力扫。
这就叫做不满足区间可加性,那让我们想想办法,把分块改造一下。
首先把整块的情况处理出来,开一个二维数组
-
首先搞个
i 和j 的二重循环,因为设定原因,j 直接从i 开始就可以了,没问题吧。 -
然后显然初始时
ans[i][j]=ans[i][j-1] -
然后再开始统计这个块的答案,直接在
ans[i][j] 上修改,就根据bufcnt 的结果修改就可以。 -
为什么设个
bufcnt 就可以了呢,我们主要是初始化答案时要做到块内数据可合并,只要bufcnt 不清空,我们就可以实现这个目的。你搞过工程的话,你可能会觉得这是一个给块统计答案的数据池,我觉得也是的。 -
由于我们为了共享数据,
又不是多测,bufcnt 在j 循环内不用清空,只要在i 循环内清空就行了,起点/左边界变了嘛,难道你又从最左边删起?编码复杂度角度考虑肯定不会这样的。
然后查询
现在再来考虑散块,我们之前的一个痛点就是区间合并时无法预测答案是否受到额外贡献,为此,我们再设立一个
容易发现其实
这就是不好直接分块的题目的一个常用办法:将每块答案用前缀和和差分处理。再用数据共享数组辅助更新答案。很牛逼吧。
但是线段树的话,这个技巧的实现就有点复杂了,这就是为什么讨论区里没有人喊线段树怎么做的原因吧(如果线段树能打破我们的痛点,那这题就太板了,不能评紫了QAQ)
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
const int sqrtn=5e3+5;
int arr[maxn];//原数组
int cnt[sqrtn][maxn];//区间1到i中x出现次数
int ans[sqrtn][sqrtn];//区间i到j的答案
int l[sqrtn];
int r[sqrtn];
int belong[maxn];
int block=0;
int tot=0;
int bufcnt[maxn];//暴力扫辅助
inline int getnum(int l,int r,int x)//差分求出块l到块r的x个数
{
return cnt[r][x]-cnt[l-1][x];
}
void init(int n,int c)
{
block=sqrt(n);
tot=n/block;
if(n%block!=0)tot++;
for(int i=1;i<=tot;i++)
{
l[i]=(i-1)*block+1;
r[i]=(i==tot)?n:(i*block);
}
for(int i=1;i<=tot;i++)
{
for(int j=l[i];j<=r[i];j++)
{
belong[j]=i;
cnt[i][arr[j]]++;
}
for(int j=0;j<=c;j++)
{
cnt[i][j]+=cnt[i-1][j];
}
}
for(int i=1;i<=tot;i++)
{
for(int j=i;j<=tot;j++)
{
ans[i][j]=ans[i][j-1];
for(int k=l[j];k<=r[j];k++)
{
bufcnt[arr[k]]++;
int buf=bufcnt[arr[k]];
if(buf%2==0)ans[i][j]++;
else if(buf!=1)ans[i][j]--;
}
}
for(int j=0;j<=c;j++)
{
bufcnt[j]=0;
}
}
}
int query(int x,int y)
{
int res=0;
if(belong[y]-belong[x]<=1)
{
for(int i=x;i<=y;i++)
{
bufcnt[arr[i]]++;
int buf=bufcnt[arr[i]];
if(buf%2==0)res++;
else if(buf!=1)res--;
}
for(int i=x;i<=y;i++)
{
bufcnt[arr[i]]--;
}
return res;
}
else
{
res=ans[belong[x]+1][belong[y]-1];
for(int i=x;i<=r[belong[x]];i++)
{
bufcnt[arr[i]]++;
int buf=bufcnt[arr[i]]+getnum(belong[x]+1,belong[y]-1,arr[i]);
if(buf%2==0)res++;
else if(buf!=1)res--;
}
for(int i=l[belong[y]];i<=y;i++)
{
bufcnt[arr[i]]++;
int buf=bufcnt[arr[i]]+getnum(belong[x]+1,belong[y]-1,arr[i]);
if(buf%2==0)res++;
else if(buf!=1)res--;
}
for(int i=x;i<=r[belong[x]];i++)
{
bufcnt[arr[i]]--;
}
for(int i=l[belong[y]];i<=y;i++)
{
bufcnt[arr[i]]--;
}
return res;
}
}
int main()
{
int n=0,c=0,m=0,last=0,ll=0,rr=0;
cin>>n>>c>>m;
for(int i=1;i<=n;i++)
{
cin>>arr[i];
}
init(n,c);
for(int i=1;i<=m;i++)
{
cin>>ll>>rr;
ll=(ll+last)%n+1;
rr=(rr+last)%n+1;
if(ll>rr)swap(ll,rr);
//cout<<ll<<' '<<rr<<endl;
last=query(ll,rr);
cout<<last;
if(i<m)cout<<endl;
}
return 0;
}
3.莫队简介
本篇不涉及莫队的在线化改造
莫队算法是一位叫莫涛的聚铑在
顺便大声喊一声:莫涛聚铑高中就读于长沙市长郡中学!
不喜欢郡系,但喜欢莫涛
我的名言
值得一提的是,还有一位聚铑,名叫范浩强,是中国人民大学附属中学的选手,他在同年大满贯。他是
更值得一提的是,我们雅礼集团早在虽然Ag,郡系还是慢了一步啊。乐死了。
其实给莫队算法起个名字还是很重要的,你说他是暴力&&剪枝,它比
另外,虽然很奇葩,但:
莫队算法是主席树(可持久化线段树)的替代算法
这句话很重要,它几乎成为莫队各种改进的指导思想。
3.1 普通莫队
我插一句,只有这个version是莫队本队提出的
现在我们进入正题,莫队怎么写?
一个典型的母题是:
-
有个长度为
n 的数组arr ,一般1 \le n\le 2\times10^{5} -
要你处理
q 个[l_{i},r_{i}] 区间的查询,范围一般同n -
可以离线(提前获取所有数据)。
我们当然可以写暴力
骗分对拍,时间复杂度显然是O(q(l+r)) ,最坏情况下直接打到O(nq) ,约4\times 10^{10} ,显然\color{#E74C3C}{Unaccepted} 。我们大多数情况下也可以写主席树,
但我们假装不行。我们此时可以构想,有两个“指针”指着数组下标,每次获取一个查询区间,然后不断将“指针”一个位置一个位置挪至对应的左右端点,每挪动一个位置就计算该位置对答案的贡献并累加或删除。这里的位置指数组的“格子”。 我们可以将累加或删除抽象到
add() 和del() 两个函数里。
设左/右指针为
我们在这里对指针与区间端点的位置关系及Solution进行讨论,这四个情况的判断代码的顺序似乎关系不大……有点绕,请大家认真体会:
-
左指针在左端点左边:
l < l_{i} 此时,左边显然对答案做了过多的贡献,所以要减去多余部分,可使用
while 来控制del(l++) -
左指针在左端点右边:
l > l_{i} 此时,左边对答案的贡献还有一部分没被统计到,要加上这一部分:
del(l++) -
右指针在右端点右边:
r > r_{i} 此时,右边对答案造成了多余贡献,减去:
del(r--) -
右指针在右端点左边:
r < r_{i} 此时,右边的贡献少了,加上:
add(r++)
答案的输出需要依照原顺序,所以对于区间,我们建个结构体维护,其中还要记录原始位置,存进答案数组时按照原数组的位置存。
你说,是不是很像滑动窗口或双指针?
在这种算法中,看不出对时间复杂度有什么优化,但我们可以将前面分块的
假设我们以
此处的排序必须要获取所有数据,所以莫队算法是离线的。
关于奇偶化排序这一小trick的证明,我太菜,讲不了一点,看普通莫队的代码吧。
习题3.1.1
P2709 小B的询问
应该算是现存的模板题,注意累加的是
还有一个有趣的地方,关于答案输出的部分,可以写完后和前面分块的代码对照一下,思考一下:为什么莫队只能离线,而分块可以在线?
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e4+5;
int arr[maxn];
int cnt[maxn];
int belong[maxn];
int ans[maxn];
int block=0;
int tot=0;
int l=1;
int r=0;
int res=0;
struct interval
{
int l;
int r;
int id;
};
interval querys[maxn];
bool operator <(interval a,interval b)
{
return (belong[a.l]^belong[b.l])?belong[a.l]<belong[b.l]:(belong[a.l]&1)?a.r<b.r:a.r>b.r;
}
void init(int n,int k)
{
block=sqrt(n);
tot=n/block;
if(n%block!=0)tot++;
for(int i=1;i<=n;i++)
{
belong[i]=(i-1)/block+1;
}
sort(querys+1,querys+k+1);
}
void add(int pos)
{
res-=cnt[arr[pos]]*cnt[arr[pos]];
cnt[arr[pos]]++;
res+=cnt[arr[pos]]*cnt[arr[pos]];
}
void del(int pos)
{
res-=cnt[arr[pos]]*cnt[arr[pos]];
cnt[arr[pos]]--;
res+=cnt[arr[pos]]*cnt[arr[pos]];
}
int main()
{
int n=0,m=0,k=0;
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
{
cin>>arr[i];
}
for(int i=1;i<=m;i++)
{
cin>>querys[i].l>>querys[i].r;
querys[i].id=i;
}
init(n,m);
for(int i=1;i<=m;i++)
{
while(l<querys[i].l)del(l++);
while(l>querys[i].l)add(--l);
while(r<querys[i].r)add(++r);
while(r>querys[i].r)del(r--);
ans[querys[i].id]=res;
}
for(int i=1;i<=m;i++)
{
cout<<ans[i];
if(i<m)cout<<endl;
}
return 0;
}
简短好记,起码比主席树好写多了。
这基本上就最简短了,再压行会不便于调试的。
3.2 带修莫队
我们发现,莫队将输入的区间排好序后,无法再更改,事实上,也不好更改。因为莫队本身只支持查询。为什么呢?你想,我们事先对所有查询进行了你还在这里改数据就是添乱! 其实你当初写
但人家主席树可是带修的,莫队气不过,所以也勉勉强强,支持一下单点修改。
你别说还真的可以
我们在
现在你可以将整个题目数据想象成一个二维坐标系,数据沿横轴展开,而纵轴是修改操作。
如果像其他文章里写的那样,硬是把没什么大不了的。
我们着重理解一下修改操作。为了保证形式一致,我们也把它抽象到一个
我们首先考虑当前修改次数
然后,我们就交换一下数组里的要修改位置的值和修改操作里的目标值。最后,别忘了移动
这里为什么要执行交换操作呢?注意到前面的
交换操作的用处就在这里,它动了一下“真格”,看似打破了莫队的理论基石,其实是保护了
所以你会发现为什么带修莫队只支持单点修改?因为它只有一个你比拿。
习题3.2.1
P1903 [国家集训队]数颜色/维护序列
上面讲解的差不多了,这里注意一些细节,比如前四个
如果你发现前四个
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6;
int arr[maxn];
int cnt[maxn];
int belong[maxn];
int ans[maxn];
int block=0;
int tot=0;
int l=1;
int r=0;
int t=0;
int res=0;
struct interval
{
int l;
int r;
int time;
int id;
};
interval querys[maxn];
struct modify
{
int pos;
int val;
};
modify ops[maxn];
bool operator <(interval a,interval b)
{
if(belong[a.l]!=belong[b.l])return belong[a.l]<belong[b.l];
else if(belong[a.r]!=belong[b.r])return belong[a.r]<belong[b.r];
else return a.time<b.time;
}
void add(int pos)
{
if(cnt[pos]==0)res++;
cnt[pos]++;
}
void del(int pos)
{
cnt[pos]--;
if(cnt[pos]==0)res--;
}
void travel(int poss,int i)
{
if(ops[poss].pos>=querys[i].l&&ops[poss].pos<=querys[i].r)
{
del(arr[ops[poss].pos]);
add(ops[poss].val);
}
swap(ops[poss].val,arr[ops[poss].pos]);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n=0,m=0,often=0,qs=0;
char op;
cin>>n>>m;
block=pow(n,2.0/3.0);
for(int i=1;i<=n;i++)
{
cin>>arr[i];
belong[i]=(i-1)/block+1;
}
for(int i=1;i<=m;i++)
{
cin>>op;
if(op=='Q')
{
qs++;
cin>>querys[qs].l>>querys[qs].r;
querys[qs].id=qs;
querys[qs].time=often;
}
if(op=='R')
{
often++;
cin>>ops[often].pos>>ops[often].val;
}
}
sort(querys+1,querys+qs+1);
for(int i=1;i<=qs;i++)
{
while(l>querys[i].l)add(arr[--l]);
while(r>querys[i].r)del(arr[r--]);
while(r<querys[i].r)add(arr[++r]);
while(l<querys[i].l)del(arr[l++]);
while(t<querys[i].time)travel(++t,i);
while(t>querys[i].time)travel(t--,i);
ans[querys[i].id]=res;
}
for(int i=1;i<=qs;i++)
{
cout<<ans[i];
if(i<qs)cout<<endl;
}
return 0;
}
关于这道题的花式问题,This may help
国集的题能给点面子嘛,就一个蓝,砹
我想水紫题
3.3 回滚/不删除/不增加莫队
一般来说,回滚莫队是针对如下情况的莫队改进的:
有一个母题:求查询区间的众数
如果要用莫队做这个题,
但
在这种情况下,我们就使用回滚莫队来回避其实还是和带修莫队一样“打假拳”
我们思考一下,怎么做能够最大程度减少复杂度大的那一个操作?
对于不删除的情况,你想啊,如果我们保证同一个块内,左端点不管,右端点是单调递增的,那我们对于右指针的删除操作是不是就可以免了?不错啊,成功了一半。
接下来是左指针,我们想办法让询问的左端点按分块的顺序排序(你应该熟悉分块顺序),使左端点所在块编号小的就排在前面处理,那左端点在大部分情况下是不是也可以不用删除操作了?好,基本成功了。所以这里不能使用奇偶化排序的卡常技巧,因为有可能右端点会因为依照奇偶排序而不能单调递增,让你
既然分了块,那当我们移动指针时,就可以考虑一块一块的处理询问(好像讲过了qwq),然后新到每个块时,就将
然后我们分三个阶段搜索:
琢磨一下,是不是只有3中涉及到删除操作了?而且告诉你吧,这个操作只要清空一个单独开的数组,删一次是满足莫队聚铑的心意。
我们开
- 每个元素出现的第一个位置
- 每个元素出现的最后一个位置
- 在询问左端点到
l 初始化位置的last 功能
对于Case 1,显然我们移动时,对于每个位置的元素
对于Case 2,虽然我们更改的也是
对于Case 3,
对于询问端点同块的情况,那你还是暴力吧,
注意
不增加的题目也有,但构造增加不为
习题3.3.1
P5906 【模板】回滚莫队&不删除莫队
虽然你们很喜欢把另一道题作为回滚莫队模板题,但我不喜欢RemoteJudge的题,它们编号不一样,打乱了通过题号的队形,所以用这道吧。qwq
正经模板题就是不一样,实现细节我都没什么好讲的了
哦,你看这变量多的,给算法发明者点赞!
AC Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+5;
int arr[maxn];//原数组、离散化后数组
int belong[maxn];//每个元素储存的块
int rs[maxn];//每个块的右端点
int ans[maxn];//每个询问的答案
int first[maxn];//每个元素在每个询问区间第一个出现的位置
int last[maxn];//每个元素在每个询问区间出现的最后一个位置
int last2[maxn];//每个元素在(询问区间-当前块)范围最后一个出现的值
int cpy[maxn];//离散化辅助数组
int lastblock=0;//上一个块编号
int block=0;//块大小
int tot=0;//块数量
int l=0;//左指针
int r=0;//右指针
int res=0;//每次询问的结果
struct interval//询问区间
{
int l;
int r;
int id;
};
interval querys[maxn];//询问区间数组
bool operator <(interval a,interval b)
{
if(belong[a.l]!=belong[b.l])return a.l<b.l;
else return a.r<b.r;
}
int main()
{
int n=0,m=0;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>arr[i];
cpy[i]=arr[i];//离散化
}
sort(cpy+1,cpy+n+1);
int len=unique(cpy+1,cpy+n+1)-(cpy+1);
for(int i=1;i<=n;i++)
{
arr[i]=lower_bound(cpy+1,cpy+len+1,arr[i])-cpy;
}//离散化end
block=sqrt(n);//分块
tot=n/block;
if(n%block!=0)tot++;
for(int i=1;i<=n;i++)
{
belong[i]=(i-1)/block+1;
}
for(int i=1;i<=tot;i++)
{
rs[i]=i*block;
}
rs[tot]=n;//分块end
cin>>m;
for(int i=1;i<=m;i++)
{
cin>>querys[i].l>>querys[i].r;
querys[i].id=i;
}
sort(querys+1,querys+m+1);
for(int i=1;i<=m;i++)
{
if(belong[querys[i].l]==belong[querys[i].r])
{
res=0;
for(int j=querys[i].l;j<=querys[i].r;j++)
{
first[arr[j]]=0;
}
for(int j=querys[i].l;j<=querys[i].r;j++)
{
if(first[arr[j]]==0)first[arr[j]]=j;
else
{
res=max(res,j-first[arr[j]]);
}
}
for(int j=querys[i].l;j<=querys[i].r;j++)
{
first[arr[j]]=0;
}
ans[querys[i].id]=res;
continue;
}
int nowblock=belong[querys[i].l];
if(lastblock!=nowblock)
{
res=0;
for(int j=l;j<=r;j++)
{
first[arr[j]]=last[arr[j]]=0;
}
l=rs[nowblock];
r=l-1;
lastblock=nowblock;
}
while(r<querys[i].r)
{
r++;
if(first[arr[r]]==0)
{
first[arr[r]]=r;
}
last[arr[r]]=r;
res=max(res,r-first[arr[r]]);
}
int p=l,tmp=0;
while(p>querys[i].l)
{
p--;
if(last2[arr[p]]==0)last2[arr[p]]=p;
tmp=max(tmp,max(last[arr[p]],last2[arr[p]])-p);
}
while(p<l)
{
last2[arr[p]]=0;
p++;
}
ans[querys[i].id]=max(res,tmp);
}
for(int i=1;i<=m;i++)
{
cout<<ans[i];
if(i<m)cout<<endl;
}
return 0;
}
想水的紫题终于到手了
请读者注意:莫队算法一般来说掌握普通普队就够了,如果本文3.2和3.3的知识学的吃力的话,先去学线段树和平衡树等同样热门的知识点可能有所帮助。因为莫队是它们的替代算法。
2024/8/20 01:00:00 初稿!完结撒花!!!