1.题目描述:点击打开链接
2.解题思路:本题利用莫队算法解决。由于是第一次学习这个算法,因此研究了比较长的一段时间才弄懂。首先,莫队算法解决的问题是无修改的离线区间查询问题。该算法实际上是由曼哈顿距离最小生成树演变来的,由于要处理m个区间,可以将这m个区间看做二维平面上的点,那么处理这m个区间就等价于让这m点连通,且总的转移代价最小。这其实就是一个曼哈顿距离最小生成树问题。
经典的曼哈顿距离最小生成树的时间复杂度是O(NlogN),莫队算法的时间复杂度是O(N^1.5)。不过本题还有一个地方,就是区间怎么处理。因为GCD相同的区间是可以合并的,因此不同的区间个数实际上是log级别的,一共有左右N个区间要处理,复杂度是O(NlogN)。下面用一个简单的例子来说明一下区间合并。
比如,数组是2,3,3,6,8,16,下标从1开始,下面的[i,j]->g表示a[i]到a[j]所有数到左端点的区间的GCD都是g。
a[1]=2: [1,1]->2, [2,6]->1
a[2]=3: [2,4]->3, [5,6]->1
a[3]=3: [3,4]->3, [5,6]->1
a[4]=6: [4,4]->4, [5,6]->2
a[5]=8: [5,6]->8
a[6]=16: [6,6]->16
从上面的例子中可以发现,以i为左端点构成的所有GCD值不等的区间实际上非常少。
处理完区间,接下来就是套用莫队算法,首先对M个查询区间进行分块排序,block=sqrt(n),首先对左端点进行排序,若相等,再对右端点排序。这样排序后可以保证总的转移代价是最小的,接下来就是进行区间转移了,举一个简单的例子来说明如何转移。
假如我们已经计算出了[L1,R1]的结果是res,下一个要计算的区间是[L2,R2],且L1<L2,R1<R2。
那么我们画图后发现,res需要减去从L1变化到L2-1的所有结果,同时要加上从R1变化到R2的所有结果。这样就得到了新的res。
3.代码:
#pragma comment(linker, "/STACK:1024000000,1024000000") #include<iostream> #include<algorithm> #include<cassert> #include<string> #include<sstream> #include<set> #include<vector> #include<stack> #include<map> #include<queue> #include<deque> #include<cstdlib> #include<cstdio> #include<cstring> #include<cmath> #include<ctime> #include<cctype> #include<functional> using namespace std; #define me(s) memset(s,0,sizeof(s)) typedef long long ll; typedef unsigned int uint; typedef unsigned long long ull; typedef pair <int, int> P; const int N=10000+5; int n,m; int block; int a[N]; ll ans[N]; struct Node { int L,R; int id; bool operator<(const Node&rhs)const { if(L/block==rhs.L/block)return R<rhs.R; return L/block<rhs.L/block; } }q[N]; int gcd(int a,int b) { return !b?a:gcd(b,a%b); } struct He { int idx; ll g; }; vector<He>vl[N],vr[N]; void preDeal() //预处理所有以i为左端点的区间,他们的右端点都存放到vr[i]中;同理处理所有以i为右端点的区间,它们的左端点都存放到vl[i]中 { for(int i=1;i<=n;i++)//i从左到右变化,计算所有的vl[i](此时i作为一个固定的右端点) { if(i==1)vl[i].push_back(He{i,a[i]});//第一个区间,只有自身 else { int curg=a[i];//当前的GCD值 int L=i; //第一个左端点 for(auto&it:vl[i-1])//每次都以i-1为右端点的区间开始扩展,得到以i为右端点的区间 { int g=gcd(it.g,curg); if(g!=curg) //g!=curg说明以i-1为右端点的这个区间不能和加入a[i]后得到的新区间合并,那么单独保存新区间。若相等则试图继续扩展左端点L vl[i].push_back(He{L,curg}); curg=g,L=it.idx; //更新curg和L值,得到新的左端点L 和 以L为左端点,i为右端点的区间的GCD值 } vl[i].push_back(He{L,curg});//最后一个区间 } } for(int i=n;i>=1;i--)//用同样的方法处理以i为左端点的所有区间 { if(i==n)vr[i].push_back(He{i,a[i]}); else { int curg=a[i]; int R=i; for(auto&it:vr[i+1]) { int g=gcd(it.g,curg); if(g!=curg) vr[i].push_back(He{R,curg}); curg=g,R=it.idx; } vr[i].push_back(He{R,curg}); } } } ll calc(int type,int L,int R)//计算区间[L,R]的结果 { ll res=0; if(!type) { int tr=R; //当前的右端点 for(auto&it:vl[R]) if(it.idx>=L)//如果当前区间的左端点>=L,则当前区间为[it.idx,tr] { res+=(tr-it.idx+1)*it.g; //这里其实用到了GCD值的传递性:如果L<L1<R,gcd([L,R])=g1,gcd([L1,R])=g2(g2≥g1),那么必有gcd([L,L1-1])=g1 tr=it.idx-1; //更新右端点 } else //如果当前区间的左端点<L,则应该被算入的区间为[L,tr],由于它是最后一个区间了,因此break { res+=(tr-L+1)*it.g; break; } } else //原理同上 { int tl=L; for(auto&it:vr[L]) if(it.idx<=R) { res+=(it.idx-tl+1)*it.g; tl=it.idx+1; } else { res+=(R-tl+1)*it.g; break; } } return res; } void solve() { for(int i=1;i<=n;i++) vl[i].clear(),vr[i].clear(); block=sqrt(n);//块数 sort(q,q+m); preDeal(); int L=1,R=0;从(1,0)点开始转移,此时res=0 ll res=0; for(int i=0;i<m;i++) { while(R<q[i].R) { R++;//向右右端点,然后计算[L,R]区间的结果,累加给res res+=calc(0,L,R); } while(R>q[i].R)//向左移动右端点 { res-=calc(0,L,R); R--; } while(L>q[i].L) { L--; res+=calc(1,L,R); } while(L<q[i].L) { res-=calc(1,L,R); L++; } ans[q[i].id]=res;//转移完毕。注意,res,L,R都不要清零 } for(int i=0;i<m;i++) printf("%I64d\n",ans[i]); } int main() { int T; scanf("%d",&T); while(T--) { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); scanf("%d",&m); for(int i=0;i<m;i++) { scanf("%d%d",&q[i].L,&q[i].R); q[i].id=i; } solve(); } }
版权声明:本文为博主原创文章,未经博主允许不得转载。