NOIP 2017 Day2 题解?

来源:互联网 发布:java解压war包命令 编辑:程序博客网 时间:2024/05/11 15:40

NOIP 2017 Day2 题解?

序言:
  不得不说,D2T3我真的码了很久,第一次还码炸了,直接弃坑重码。但是经过我不懈的努力【说白了就是菜】,我终于把它改对了!!!!!!!!
   至于我的修改历程,我会在这篇题解的下面详细描述!这不失为一种做题经验以及错题积累。
  其实,T1、T2真的很水(1AC),我考试的时候第二题被卡在了一个DP顺序性的问题真的伤。同时,由于sc同学对T2的疑问引发了我对T2(DP)一些更深层次的思考,后面一一介绍。


T1

#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<vector>#include<queue>#include<cstdlib>#define LL long longusing namespace std ;queue<int>q;const int maxn=1010;struct edge{    int v,nxt;}e[maxn*maxn*2];int head[maxn],can[maxn],cnt=1,n,cnt2=0;LL h,r,x[maxn],y[maxn],z[maxn];bool go[maxn];LL sqr(LL x){    return 1ll*x*x;}void add(int u,int v){    e[cnt].v=v;    e[cnt].nxt=head[u];    head[u]=cnt++;}LL Calc(int i,int j){    LL tmp=sqr(x[i]-x[j]);    tmp+=sqr(y[i]-y[j]);    tmp+=sqr(z[i]-z[j]);    return tmp;}void init(){    memset(go,0,sizeof go);    memset(head,0,sizeof head);    cnt2=0,cnt=1;    scanf("%d%lld%lld",&n,&h,&r);    for (int i=1;i<=n;i++)    {        scanf("%lld%lld%lld",&x[i],&y[i],&z[i]);        if (z[i]>=h-r)            can[cnt2++]=i;    }    for (int i=1;i<=n-1;i++)        for (int j=i+1;j<=n;j++)            if (i!=j)            {                LL w=Calc(i,j);                if (w<=sqr(r<<1ll))                {                    add(i,j);                    add(j,i);                }            }}void spfa(int s){    while (!q.empty()) q.pop();    q.push(s); go[s]=1;    while (!q.empty())    {        int u=q.front(); q.pop();        for (int i=head[u];i;i=e[i].nxt)        {            int v=e[i].v;            if (!go[v])            {                go[v]=1;                q.push(v);            }        }    }}void solve(){    for (int i=1;i<=n;i++)    {        if (go[i]&&(z[i]>=h-r)) break;        if (!go[i]&&z[i]-r<=0)            spfa(i);    }    for (int i=0;i<cnt2;i++)        if (go[can[i]])        {            printf("Yes\n");            return ;        }    printf("No\n");}int main(){    int T;    scanf("%d",&T);    while(T--)    {        init();        solve();    }    return 0;}// 注:考试代码,码丑勿喷。orz

T2

  一开始我对这题其实并没有什么特殊的感觉(因为我一遍就A了),但是sc问了我一些问题之后,我对这个DP有了更深看的理解。

  首先,这道题目肯定是状压这不用多说(不懂的请左转看我博客↑),这点很容易想到。其次就是怎么转移以及转移的顺序都是我们需要考虑的,因为这些的证明决定了这个DP(搜索)是否可行。
  按照我一开始的想法就是,因为搜索是没有顺序性的,也就决定了最优方案一定会被选出来,并且因为最优方案是最优的,所以它不会被覆盖掉(别小看这一点,连sc大佬都没能想明白)。也就是说,最优方案一定会被枚举到,怎么证明这一点呢?
  要证明最优方案一定会被枚举到,不如换个角度,来证明最优方案不可能不会被枚举到。那么最优方案不会被枚举到的情况是什么样的呢?我们来画图举例。

  上面的情况正是一种最优方案“可能”被cut掉的情况。但是如何证明最终方案不会被cut掉呢?
  我们可以这样考虑,由于key是最后被加入的,导致了当前最优方案不是最终方案。但是显然,最终方案同样是不会被cut掉的,因为这两个不是相同的子集,所以最终方案的子集并没有因此被覆盖掉。所以这并不能成为cut掉最终方案的理由。那么要什么样的图才能cut掉最终方案呢?答案就是子集相同的其他方案cut掉了最终方案内的。

  简单来说,上面要说明的是:形状(这里用这个词比较形象)不同的子集不会互相覆盖。

  到了这里,问题就显然了。就是相同的子集,最终方案的子集到底是不是最优的呢?答案是显然的。我们把根节点给删除后,最终方案就变成了几个互不相关的子问题。这些子问题的根节点就是原本的根节点的子节点们。因为这些子节点是互不相关的,所以每个子问题同样也一定是最优的,否则最终方案就不是最优的。至于能否选出最优的子集,这个只取决于你的搜索是否正确。所以到了这里,被cut掉的,只可能是那些不可能存在于最终方案的子集。

  同样,上面要说明的是:相同形状的子集一定能选出最优的,并且只有最优的才可能构成最终方案。

  最终方案与最优子集的构成同理。

  我觉得我的说明还是很模糊,所以,看斜体部分就好了。


  以上就是我对这题用搜索完成DP的证明,想要有更深层次理解的,可以右转去看这题→NOIP2016愤怒的小鸟(我觉得洛谷应该给我广告费),同样是状压DP经典题目
  其实这两题都属于经典DP,是基本操作。只是我太菜而已。

  废话了半天,代码忘贴了。

#include<iostream>#include<cstdio>#include<cstring>#include<cstdlib>#include<algorithm>#include<queue>#include<vector>using namespace std ; const int N=13;const int M=1010;int n,m;int f[1<<N],w[N][N],dis[N];bool len[N][N];void init(){    scanf("%d%d",&n,&m);    for (int i=1;i<=n;i++)         for (int j=1;j<=n;j++)            w[i][j]=999999999;    memset(len,0,sizeof len);    for (int i=1;i<=m;i++)    {        int a,b,c;        scanf("%d%d%d",&a,&b,&c);        if ((!len[a][b])||w[a][b]>c)        {            len[a][b]=len[b][a]=1;            w[a][b]=w[b][a]=c;        }    }}void dfs(int s){    for (int u=1;u<=n;u++) // 枚举每个选中过的点    {        int su=1<<(u-1),tu=u-1;        if (!((s>>tu)&1)) continue ;        for (int v=1;v<=n;v++) if (v!=u)        {            int sv=1<<(v-1),tv=v-1;            if (len[u][v]&&(!((s>>tv)&1))) // 枚举一个要更新的点            {                if (f[s|sv]>f[s]+w[u][v]*dis[u])                {                    int t=dis[v];                    f[s|sv]=f[s]+w[u][v]*dis[u];                    dis[v]=dis[u]+1;                    dfs(s|sv);                    dis[v]=t;                }            }         }    }}void reset(int x){    for (int i=1;i<=n;i++) dis[i]=n+1;    for (int i=0;i<(1<<n);i++) f[i]=999999999;    dis[x]=1;    f[1<<(x-1)]=0;}void solve(){    int ans=999999999;    for (int i=1;i<=n;i++) // 枚举起始点    {        reset(i);        dfs(1<<(i-1));        ans=min(ans,f[(1<<n)-1]);    }    printf("%d\n",ans);}int main(){//  freopen("1.in","r",stdin);//  freopen("1.out","w",stdout);    init();    solve();    return 0;}

写代码小提示:加一点注释会使思路更清晰(神犇勿看)


T3

  终于写到这里了。这道题我的感悟更深刻了。【愈发凸显自己有多菜】

  先说一下这道题目的多种方法吧。

  • 算法一:splay暴力做。每一行看成一颗splay树,每一列也看成一颗。只要支持删除,插入,查询(都是单点的)就可以完成这个算法。
  • 算法二:treap+线段树。与算法一同理。
  • 算法三:直接线段树,动态开节点(by ljh)。在线段树上跑二叉查找。同样支持查询,删除,插入。由于插入只需要在最后插入,相当于动态开节点,删除相当于清空节点,因为查询个数不多于3×105,所以开的节点不会太多。
  • 算法四:树状数组,同样也是动态开节点(by myself)。奇妙的是,在树状数组里不能跑二叉查找,但是能跑倍增?!,道理同上,但是细节多了很多,常数可能比较大(应该是我码搓了)。按照预期,应该是N的空间,O(N×logN2)的时间。但是,这可能变成码农题。

算法三可以右转看→ljh的博客

  前三种算法我只是略作了解,就不多赘述。我只对我码的算法详细介绍。

  首先,把原问题分解成两个问题:calculate Red and Blue?

  红色代表没有被拿出来的数,蓝色表示新插入的数。我们可以把红色和蓝色分开考虑。
  首先,我们考虑红色部分的计算。对于每一行的询问单独考虑,每一行内按时间顺序加入,每次查找并且删除一个数,这个数是xi=1Treei,这个数所在的下标就是x。
  接下来我们要做的就是如何找到这个x。最直接的办法就是二分位置,然后用sum统计个数,这显然具备二分的条件——单调递增。但是这样就变成log2的了。虽然能过,但是我们要思考的是,为什么线段树能够在树上做二叉查找,而树状数组不行呢?那是因为树状数组是线性的记录方式。
  但是,树状数组就真的不行吗?答案是可以的。怎么样能够线性的查找呢?我们可以发现,树状数组记录的信息只与lowbit有关系。(不懂得可以右转→深入学习树状数组)那么,最终的选择模型一定可以是这样的,或者说就是这样的,如图。

  也就是说,第一步是最大的,后面每一步都不可能跟第一步一样大或者更大。
  这是很容易证明的,因为如果后面有一步比第一步大,第一步一定可以变成原来的两倍,直到没有任何一步跟第一步一样或者是更大。这无非就是二进制的性质而已。
  同理,后面每一步也都是相同的道理。其实我想要说明的就是,每一步的大小都是单调递减的。所以说,我们可以从最大的一步开始枚举,知道最小的一步,这样我们一定能够找到这个x。

  简单来说,就是每个数都是二进制,从最高位开始枚举。由于树状数组里的val是单调递增的,所以没毛病

  那么现在问题就是要求蓝色部分如何计算了。我们先把每一行单独考虑,因为行与行之间显然是不会互相影响的,除了最后一列。因此,我们把最后一列也单独看成一行,那么就会把一幅图变成很多条链。同时,这些操作分别可以转化成对这些链的操作。
  到了这里,显然就可以用splay来直接维护了。但是,作为一个不会splay的蒟蒻选手应该怎么办呢?其实这题不一定需要splay的。我们可以用树状数组暴力模拟splay,只需要维护三种操作,插入,查找,删除。
我们来一种一种的解释

1.插入

  如何实现呢?直接在树状数组里新开一个节点,并且用一种玄学操作计算这个点的值,至于为什么,请自行脑补。

int bj=size&-size;for (LL i=size-1;(i&-i)<bj&&i;i-=i&-i)    bst_c[x][size]+=bst_c[x][i];// bst就是树状数组的意思;bj就是边界

2.查询

  查询就和上面说的一样,利用树状数组的特点,从最高位开始确定,就可以实现一个log的做法。

3.删除

  这个更加简单,直接把这个点设为0就ok了。然后把这个位置-1。

  有了这三种操作,就可以把蓝色部分的操作变成两种情况。
  如果是在最后一列,我们就把这个位置的数字删除,并且在最后一个插入这个数字。否则就在原序列中删除这个位置的数字,然后插入在最后一列的最后一个,并且删除最后一列中这一行的这个数字然后把它插入到这行中的最后一个。

  很多人会想问,“这样开不也还是N2的空间吗?”不不不,因为他最多询问N次,所以如果动态开节点,我们最多只需要开2N个,所以远远不会爆炸。

先贴个代码压压惊

#include<iostream>#include<cstdio>#include<cstring>#include<cstdlib>#include<algorithm>#include<queue>#include<vector>#define LL long longusing namespace std ; const LL N=300300;struct Tquery{LL x,y,ans,i;Tquery(){x=y=ans=i;}}qry[N];LL tail[N],tree[N],a[N],fuck[N]/*粗鄙之语*/,n,m,q;vector<LL>bst_v[N],bst_c[N];bool cmpx(Tquery a,Tquery b){if(a.x==b.x)return a.i<b.i;return a.x<b.x;}bool cmpi(Tquery a,Tquery b){return a.i<b.i;}void add(LL x,LL val){for(;x<=m;x+=x&-x)tree[x]+=val;}void add_(LL x,LL tmp,LL val,LL n){for(;tmp<=n;tmp+=tmp&-tmp)bst_c[x][tmp]+=val;}LL sum(LL x){LL re=0;for(;x>0;x-=x&-x)re+=tree[x];return re;}void calc_red() // log^2 --> log{    sort(qry+1,qry+1+q,cmpx);    LL pos=1;    for (LL i=1;i<=m-1;i++)         add(i,1); // first set    for (LL i=1;i<=n&&pos<=q;i++)    {        while (qry[pos].x<i) pos++;        if (qry[pos].x>i) continue;        LL l=pos,r,c=0;        while (qry[pos+1].x==i) pos++; r=pos;        for (LL j=l;j<=r;j++)        {               LL tmp=0,re=qry[j].y;            for (LL k=20;k>=0;k--)            {                LL sk=1<<k;                if ((tmp|sk)>m-1) continue;                if (re>tree[tmp|sk])                 {                    re-=tree[tmp|sk];                    tmp|=sk;                }            }            if (tmp+1>m-1||re>1)  // the meaning is no ans                continue ;            tmp++; // it's the real location            qry[j].ans=tmp+(qry[j].x-1)*m;            a[c++]=tmp; // for call back            add(tmp,-1);        }        for (c--;c>=0;c--)            add(a[c],1);    }    sort(qry+1,qry+1+q,cmpi);}LL Find_del(LL x,LL pos){    // Find the location of the pos-th    LL tmp=0,re=pos,size=fuck[x];    if (x!=n+1) re-=tail[x]+1;    for (LL k=20;k>=0;k--)    {        LL sk=1<<k;        if ((tmp|sk)>size) continue ;        if (re>bst_c[x][tmp|sk])        {            re-=bst_c[x][tmp|sk];            tmp|=sk;        }    }    re=bst_v[x][++tmp];    // Del the pos-th    add_(x,tmp,-1,fuck[x]);    bst_v[x][tmp]=0;    if (pos>tail[x]+1) tail[x]++;    return re;}void Insert(LL x,LL val){    fuck[x]++;    bst_c[x].push_back(1);    bst_v[x].push_back(val);    LL size=bst_v[x].size()-1;    // updata the new point    LL bj=size&-size;    for (LL i=size-1;(i&-i)<bj&&i;i-=i&-i)        bst_c[x][size]+=bst_c[x][i];}LL p;void calc_blue(){    tail[n+1]=m-n-1;    for (LL i=1;i<=n;i++) Insert(n+1,i*m);    for (LL i=1;i<=q;i++)    {        if (qry[i].x==n+1)        {            p=Find_del(n+1,qry[i].y);            Insert(n+1,p);        }else        {            tail[qry[i].x]--;            p=qry[i].ans?qry[i].ans:Find_del(qry[i].x,qry[i].y);            Insert(qry[i].x,Find_del(n+1,qry[i].x));            Insert(n+1,p);        }        qry[i].ans=p;    }}void print(){    for (LL i=1;i<=q;i++)        printf("%lld\n",qry[i].ans);}void solve(){    calc_red();     calc_blue();    print();}void init(){    scanf("%lld%lld%lld",&n,&m,&q);    for (LL i=1;i<=q;i++)    {        scanf("%lld%lld",&qry[i].x,&qry[i].y);        qry[i].i=i;        qry[i].ans=0;        if  (qry[i].y==m)        {            qry[i].y=qry[i].x;            qry[i].x=n+1;        }    }    for (LL i=1;i<=n+1;i++)    {        bst_v[i].push_back(0);  // vector从1开始要补零         bst_c[i].push_back(0);        tail[i]=m-1;        fuck[i]=0;    }}int main(){    init();    solve();    return 0;}

  最后,再讲一个小故事。
  为什么我会有这么一个粗鄙的变量?因为我那时已经改这题改得心态爆炸了。我最后才发现我的边界开得有问题,会很难计算,所以我干脆就随便开了个变量一次次记录我的边界到底修改没。然后就过了?!! In surprise~。
  最最最后,再再再讲一个小故事——线段树不到一百行就过了。头铁的我硬是要打树状数组。【为树状数组正名!】

原创粉丝点击