• NOIP 真题选讲


     推荐生要凉辽

    这可能是我更新的最后一篇博客

    代码什么的有时间再说吧,先讲思路。(已搞定前三题代码)

    首先先看一下线段覆盖题。我们有一个区间,要用线段覆盖整个区间。

    这个是线段的覆盖简图。我们如何选取最少的线段来覆盖整个区间呢?先将右端点排序,再将左端点贪心一下。

    比如:

    但在这个题里面覆盖的是格子,不一定连续。这就比较复杂了,我们要想点办法。我们线段覆盖解决的是格子连续的情况。所以我们要先证明第一行每个格子里的蓄水池覆盖第n行的格子是连续的。

    如果从i无法流到j,而能流到j-1和j+1,从别的城市可以流到j,那么这两条路线一定有一个交点,那么i也可以从这个交点流到j,这与条件矛盾。

     然后这就是一个线段覆盖的问题了。因为在第1行的城市对第n行的城市覆盖的区间是连续的,就相当于在第n行进行线段盖。

       我们解决能否到达时,就用dfs/bfs求一遍,顺便把第一行每个城市覆盖的第n行城市求出来,然后进行线段覆盖。

       这样会T一个点,所以我们要有个优化:第一行只有比两边都搞的点才进行搜索,如果有一边比这个点高,那这个点所覆盖的区域肯定在比这个点高的子集里面。

       以及注意一下n=1的情况,下面代码里会讲

       

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<algorithm>
    #include<cstring>
    #include<queue>
    using namespace std;
    int n,m,a[501][501],k,ans,ans2;
    int dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
    struct xianduan
    {//处理线段 int l, r; xianduan()//构造函数(结构体初始化) {l=2147483647; r=-1; } }xd[550];//记录第一行每个点覆盖的第n行的区间 bool vis[501][501],vis2[501];//vis记录在一次搜索中走过的点,vis2记录最后一行被覆盖的点 bool check() {bool c=1; for(int i=1;i<=m;i++)//判断最后一行是否都被覆盖 { if(!vis2[i]){c=0;ans2++;}//如果有没被覆盖的,就统计个数 } return c; } struct dl{ int x,y; dl(int xx,int yy):x(xx),y(yy){}//构造函数*2 }; bool hf(int x,int y,int xx,int yy)//广搜时判断能不能走 { if(xx<1||yy<1||xx>n||yy>m)return 0; if(a[xx][yy]>=a[x][y])return 0; if(vis[xx][yy])return 0; return 1; } queue <dl> q; void bfs(int st)//广搜模板 { if(a[1][st]<a[1][st-1]||a[1][st]<a[1][st+1])return ; memset(vis,0,sizeof(vis));//每次搜索记得清空,不然可能会出现什么意外 vis[1][st]=1; while(!q.empty()) {dl ex=q.front(); q.pop(); for(int i=0;i<4;i++) {int xx=ex.x,yy=ex.y; xx+=dx[i];yy+=dy[i]; if(hf(ex.x,ex.y,xx,yy)) {vis[xx][yy]=1; q.push(dl(xx,yy)); if(xx==n) {if(yy>xd[st].r){xd[st].r=yy;}//记录xd[i] if(yy<xd[st].l){xd[st].l=yy;} vis2[yy]=1;//因为vis每次都要清空,所以才用vis2来记录 } } } } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&a[i][j]); for(int i=1;i<=m;i++) bfs(i); if(check()) {int left=1,r_max=0; while(left<=m) { for(int i=1;i<=m;i++) {if(xd[i].l<=left)r_max=max(r_max,xd[i].r); } ans++; left=r_max+1; } printf("1 %d ",ans); } else { if(n==1)printf("1 %d ",ans2);//n=1时的特判。因为当n=1时,每次出队的点的vis2并不会被标记,所以没有标记的点(1,j)就是建蓄水池的点 else printf("0 %d ",ans2); } }

    不枉我肝了近90行,wa了3次

    我们先枚举第二个客栈j,找到j前面的第一个(从右往左)价格合理,颜色符合要求的客栈i。那么在i前面,颜色和i一样的客栈的数量+1就是当前j的方案数,如果在i后面,j前面还有颜色相同的客栈,那么这些客栈的方案数与j相同。

    上面的式子在说些什么呢?就是当前以i为右端点的方案数,如果当前i客栈这种颜色出现的最大的地方(不包括i)k要比当前价钱合理的最大的地方t位置靠后的话,就是k的方案数。

    当出现这种情况时:

     更多细节请见代码:

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<algorithm>
    #include<cstring>
    #include<queue>
    const int MAXN=2000001;
    using namespace std;
    int p,k,n,pre[MAXN],pos[51],tot[51],res[MAXN];
    struct kezhan{
        int col,mon;//col是coler,mon是money
    }a[MAXN];
    int main()
    {
        scanf("%d%d%d",&n,&k,&p);
        for(int i=1;i<=n;i++)
         {scanf("%d%d",&a[i].col,&a[i].mon);
         if(a[i].mon<=p)pre[i]=i;//因为这里每个下标i对应的都是不一样的数,所以可以在输入中预处理
         else pre[i]=pre[i-1];
         }
        for(int i=1;i<=n;i++)
        {  
           if(pre[i]<pos[a[i].col])//先dp,再统计tot和pos
            res[i]=res[pos[a[i].col]];
           if(pre[i]>=pos[a[i].col])
            res[i]=tot[a[i].col];
            pos[a[i].col]=i;
            tot[a[i].col]++;
        }
        long long ans=0;
       for(int i=1;i<=n;i++)//最后答案是每个i作为右端点的方案数的和
        ans+=res[i];
      printf("%lld",ans);
    }

    交了6次才A,嘤嘤嘤

    首先考虑并没有加速器的情况。

     设wait[i]表示到i最晚的游客到达的时刻,设arrive[i]表示车到i点的时刻。 

     则arrive[i]=max(arive[i-1],wait[i-1])+d[i](d[i]就是路程)

    现在我们加上加速器。

     加速器可以减少距离,但并不能减少游客的到达时间,所以当游客的到达时刻大于汽车的到达时刻的时候,使用加速器就没有意义了。我们要让加速器有效,就选取arrive[i]比wait[i]大的点,进行加速,然后重复上面的过程,一直到加速器用完或是没有满足条件的边为止。

     因为代码的维护不会写(树状数组优化复杂度神马的)然后翻洛谷题解,有了些新的收获。

    1.这个题的维护不需要优化,因为o(n^2)也能过

    2.这里贪心的是造福最多人民群众的区间(可能上课没好好听漏了点思路qwq我错惹)

     代码什么的可能1个月之后才会出吧。

     先理一理思路。

    首先,我们预处理出车在每个点的等待时间。

    递推计算每个点的arrive,并且统计在哪里会产生arrive大于wait并且用了加速器后能造福最多的人民群众,将这一段区间进行维护,一直到加速器用完为止。

    维护的话,因为对一个[i,i+1]使用加速器后,可能影响到后面的arrive。如果后面的arrive>wait,就说明这里使用加速器会影响后面,否则不会。就跳出循环。

    我们看到题,就会发现di的定义别扭的一批,所以我们改一下,有助于后面我们乱搞。

    我们把di的定义改为从i-1到i的距离,读入就改为:

    for(int i=2;i<=n;i++)
     scanf("%d",&d[i]);

     注意是从i=2开始读入。

      我们要选择造福人数最多的点,所以我们边读入边处理到达每个点的人数。在使用加速器的时候,每次都从头开始更新造福人最多的点(注意memset(不写memset爆0警告)),选出最多的点进行更新。

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<algorithm>
    #include<cstring>
    using namespace std;
    int goal[1002],d[1002],m,k,n,wait[1002],arr[1002],yx[1002],max_i,max_yx;//yx[i]是选择在i-1到i的距离-1,造福的人数,arr[i]为到达第i个点的时刻,wait[i]是每个点要等多久
    struct c{
        int time,st,aim;
    }ck[100001];
    int main()
    {
        scanf("%d%d%d",&n,&m,&k);
        for(int i=2;i<=n;i++)//注意i从2开始
        scanf("%d",&d[i]);
        for(int i=1;i<=m;i++)
        {scanf("%d%d%d",&ck[i].time,&ck[i].st,&ck[i].aim);
         wait[ck[i].st]=max(wait[ck[i].st],ck[i].time);//处理每个点的等待时间    
         goal[ck[i].aim]++;//统计每个点到达的人数
        }
        for(int i=2;i<=n;i++)
          {arr[i]=max(arr[i-1],wait[i-1])+d[i];
          }
        while(k)//开始使用加速器
        {   max_yx=0;max_i=0;
           memset(yx,0,sizeof(yx));//一定一定要写
            for(int i=n;i>=2;i--)//倒着写递推求yx,可以减少一重循环
            {
             if(d[i]==0){yx[i]=0;}//如果有d被减到了0,就肯定不能再减了,所以也就不会造福人了
             else
             {if(arr[i]>wait[i])
              {yx[i]+=goal[i];
               yx[i]+=yx[i+1];//这样的i可以影响到i+1
               if(yx[i+1]==0&&wait[i+1]==0)
               yx[i]+=yx[i+2];//注意这里,如果某个点i+1既没有人要去,又没有人的起点是它,那么arr[i+1]一定大于wait[i+1],而且yx[i]是可以加上yx[i+2]的,所以要做特判(这个点值20分,题解里有的没有写qwq)说实话应该再来个循环判断的,但是我懒qwq
              }
              else yx[i]=goal[i];//如果无法影响到i+1,那么造福的人数就只有到达i的人
             } 
            }
            for(int i=1;i<=n;i++)//选最大
            {if(yx[i]>max_yx)
              {max_yx=yx[i];
               max_i=i;
              }
             } 
             if(max_i==0)break;
            d[max_i]--;
             k--;
            for(int i=max_i-1;i<=n;i++)//简单粗暴的维护
             arr[i]=max(arr[i-1],wait[i-1])+d[i]; 
       }
        int sum=0;
        for(int i=1;i<=m;i++)
        {
            sum+=arr[ck[i].aim]-ck[i].time;
        }
        printf("%d
    ",sum);
    }

          这两个人轮流来,所以在决策的时候就很麻烦,而且没有后效性。所以我们把A和B的决策算做一步。这样决策就有后效性了。

     

    设每经过一遍决策之后到达的城市与当前所在的城市连一条边,那每个城市只有一条出边,这就是棵平衡树。dalao都说用双向链表什么的,可惜我只见过set。

    我们从出发点往上跳,看能否跳到终点,当在每一层最大的边权大于x的时候就不能跳了。这里我们用树状倍增处理。

    树状倍增:

    设fa[i][j]表示i的第2^j个祖先。max[i][j]=max(i,fa[i][j])表示i到fa[i][j]的最大边权。二分比较。

    二分答案+check。(仿佛看见当年跳石头的影子)为什么捏?你看这毒瘤数据范围,又大又圆。

    怎么检查?

    检查点建立的越接近首都,覆盖的路径就越多。所以我们希望检查点都建立在首都的儿子节点上。

       但是有的军队可能不能在t的时间内走道首都的儿子节点,那我们不动它,把所有军队能走道的节点标记下来。每个标记的节点的子树就被控制住了。但如果还有节点没有封住,怎么办?(就像下面这样)

       我们先假设军队在建造完成之后还可以移动。每个的剩余移动时间为d[i],让最少的不再移动,让其他的移动到首都。再通过首都移动到没有覆盖住的点。这样我们按照剩余时间排序,看军队是否能覆盖剩余检查点。

      

    思路:最短路。(因为最少时间啊)老师表示这玩意就是n个拼起来不想讲

    暴力预处理每个状态,然后跑最短路(如果图不连通,就无解)

     

    我们像上面这样把屏幕分成几个格子,看小鸟能到达哪些格子。

     

    依旧是二分时间+判定。

    二分答案后就是考虑树上两个点的距离是否小于某个数。(不加虫洞)(用树状倍增)

    来我们加上虫洞。

     在不加虫洞时,有些能完成,而有些完不成。要保证都能完成,我们把虫洞放在完不成的路径的交上。(选权值最大那条边)放完之后看能否都能完成。

    details:

    找交:看路径是否被覆盖n次。我们覆盖路径的时候,因为每次路径的次数要+1,暴力加复杂度就炸了。所以我们用前缀和的方式来整。

    空间???真是鬼畜。(玄学)

    开a[100]记录???

    正解:设

    number ,prenumber, count

    if(number!=prenumber)count--;

    else count++;

    if(count==0)prenumber=number;

    ans=prenumber;

    prenumber是上一个读入的数,number是当前读入的数,重复这个玄学操作一直到最后。

    这里的数据保证有解。(因为这个正解算法判不了无解,如果有毒瘤数据就裱出题人好了(小姐姐说的qwq))

    首先,你建不出来补图

    妈耶我又凉了

    如果两个点之间没有边,那么它在补图里有边。如果有边,那在补图里就没有边。

    从1开始,把和它没有边关系的点加入队列。一个一个处理队列里面的点。当队空时,就处理完了一个联通块。如果还有没有入队过的点,就从这个点开始,进行相同的操作。直到所有点都入过队。

    我们考虑一下优化。用链表。

  • 相关阅读:
    LG4762 Virus synthesis
    深入浅出Vue.js(一) 变化侦测
    LRU
    时间复杂度 & 空间复杂度
    rem的实现原理
    瀑布流布局
    ts-不懂强记
    Notification
    Grid & Flex
    交换两个变量的值
  • 原文地址:https://www.cnblogs.com/lcez56jsy/p/10807477.html
Copyright © 2020-2023  润新知