HDU 5381 The sum of gcd (2015年多校比赛第8场)

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();
    }
}

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-10 23:53:22

HDU 5381 The sum of gcd (2015年多校比赛第8场)的相关文章

hdu 5381 The sum of gcd 2015多校联合训练赛#8莫队算法

The sum of gcd Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) Total Submission(s): 23    Accepted Submission(s): 4 Problem Description You have an array ,the length of  is Let Input There are multiple test cases.

HDU 5411 CRB and Puzzle (2015年多校比赛第10场)

1.题目描写叙述:pid=5411">点击打开链接 2.解题思路:本题实际是是已知一张无向图.问长度小于等于m的路径一共同拥有多少条. 能够通过建立转移矩阵利用矩阵高速幂解决.当中,转移矩阵就是输入时候的邻接矩阵,同一时候多添加最后一列,都置为1.表示从i開始的,长度不超过M的路径的答案总数(最后一行的1~n列为全0行,能够理解为空集),那么把转移矩阵自乘M-1次后就是路径长度为M的转移矩阵(这里的路径长度指的是顶点的个数.顶点=边数+1,因此仅仅须要乘M-1次). 为何便于求和.能够设置

HDU 5414 CRB and String (2015年多校比赛第10场)

1.题目描写叙述:点击打开链接 2.解题思路:本题要求推断字符串s是否能通过加入若干个字符得到字符串t. 首先,能够知道,s必须是t的一个子串(注意:不是连续子串). 第二.因为插入的新字符和它前面的字符c不同.因此假设t中有cnt个连续的c.那么在s中也必须有cnt个连续的c.因此.仅仅要能够满足这2个条件,就一定能够成功实现转化. 那么该怎样做呢?两者能够结合起来推断,用i,j分别表示s,t串中当前扫描的字符的下标.首先从字符串t開始扫描,看第一个字符c是否连续,一直到不连续为止,那么依据上

HDU 5407 CRB and Candies (2015年多校比赛第10场)

1.题目描述:点击打开链接 2.解题思路:本题要求LCM(C(n,0), C(n,1),..., C(n,n)),官方题解是转化为求解LCM(1,2,3,...n+1)/(n+1),然而这种做法还是觉得太陌生,不妨试着用学过的唯一分解定理去做. 首先,求这n+1个数的LCM,实际上就是求所有小于n的素数中,对于每一个素数Pi,哪一项的指数最大,然后把他们连乘起来即可得到LCM值.因此,问题转化为确定每一个pi的最大指数.这里要用到Kummer定理来解决,Kummer定理告诉我们这样一个事实:p恰

hdu 5381 The sum of gcd(线段树+gcd)

题目链接:hdu 5381 The sum of gcd 将查询离线处理,按照r排序,然后从左向右处理每个A[i],碰到查询时处理.用线段树维护,每个节点表示从[l,i]中以l为起始的区间gcd总和.所以每次修改时需要处理[1,i-1]与i的gcd值,但是因为gcd值是递减的,成log级,对于每个gcd值记录其区间即可.然后用线段树段修改,但是是修改一个等差数列. #include <cstdio> #include <cstring> #include <vector>

hdu 5381 The sum of gcd 莫队+预处理

The sum of gcd Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) Problem Description You have an array A,the length of A is nLet f(l,r)=∑ri=l∑rj=igcd(ai,ai+1....aj) Input There are multiple test cases. The first line

HDU 5381 The sum of gcd 莫队暴力

链接 题解链接:http://www.cygmasot.com/index.php/2015/08/15/hdu_5381/ 题意: 给定n长的序列 下面n个数给出这个序列 m个询问 下面m行给出询问的区间. 对于一个询问,输出这个区间内的任意子段的gcd 和. 思路: 因为一个数的gcd只会不变或下降,下降一次至少减半,下降至多32次,所以处理出每个数连续相同的gcd的区间. 然后暴力跑莫队. #pragma comment(linker, "/STACK:1024000000,1024000

HDU 5381 The sum of gcd

对于每个i,求出若干区间[l1,r1],[l2,r2],[l3,r3]...满足gcd(l1~i)~gcd(r1~i)一样,gcd(l2~i)~gcd(r2,i)一样... 则以i为右区间的所有gcd和为sum[i] = (r1 - l1 + 1) * g1 + (r2 - l2 + 1) * g2 + ... 同理求出i右边的一段段同gcd区间[l11,r11],[l22,r22],[l33,r33]... 然后将询问按左区间排序,int p初始设为1,对于p <= i < L,要消除i对所

hdu 5381 The sum of gcd(线段树等差数列区间修改+单点查询)

题意: 给出一个数组a,叫你每次询问如下等式的值. f(l,r)=∑ri=l∑rj=igcd(ai,ai+1....aj) 解析: 思考了很久终于理解了学长的思路 给你一个序列,这个序列的子序列gcd的个数不会超过logN个(N为每个数字,最大能取到的范围) 因为求gcd是递减的,每次至少除以2,所以gcd的个数只会有logN个. 然后让我们来看看题目要求的是什么. 所有子区间的gcd的和. 比如[1, 5]这个区间可以分解成如下子区间. [1, 1] [1, 2] [1, 3] [1, 4]