NOIP2012复赛提高组day2(A:同余方程 B:借教室 C:疫情控制)

来源:互联网 发布:任我行软件txyapp 编辑:程序博客网 时间:2024/05/22 13:05

A题:

这题在xiaoC大人的题库中仍旧有出现过
然而不知道为什么还是懵逼了好久
这题是扩展欧几里得。。
我们在这里给出扩展欧几里得的证明方法
本题即 求 ax+by=1的解
首先我们要清楚的是一件事。。
那就是当gcd(a,b)!=1的时候,这个方程是不存在解的,你别问我为什么。。。
so~~~
①显然当 b=0,gcd(a,b)=a。此时 x=1,y=0;
②当ab!=0 时
  设 ax1+by1=gcd(a,b);
  bx2+a%b*y2=gcd(b,a%b);
  根据欧几里德原理可以得到 gcd(a,b)=gcd(b,a%b);
  则:ax1+by1=bx2+a%b*y2;
  即:ax1+by1=bx2+(a-(a/b)*b)y2=ay2+bx2-(a/b)*by2=ay2+b(x2-a/b);
  那么: x1=y2; y1=x2-(a/b)*y2;
这样我们就得到了求解 x1,y1 的方法,而后递归求解就行了
于是代码如下

#include<bits/stdc++.h>using namespace std;void ex_gcd(int a,int b,int &x,int &y){    if(!b){        x=1;y=0;        }else{        ex_gcd(b,a%b,x,y);        int tmp=x;        x=y;        y=tmp-a/b*y;     }}int main(){    int a,b,x,y;    scanf("%d %d",&a,&b);    ex_gcd(a,b,x,y);    x%=b;    while(x<0)x+=b;//PS:x可能为负数    printf("%d\n",x);    return 0;}

B题:

这是一道十分漂亮的题目
这题博主在此介绍三种解法:

第一种解法
线段树 **(区间更新,区间查询)**O(mlogn);【系数较大】
(最无脑的方法,博主考试时写的)
不要以为这题数据100W就用不了线段树
(不要跟我说你们那些撮代码)
这题的update和query完全可以放在一块
于是系数就被调的比较低了
于是线段树也没有什么好说的
纯模拟

//PS:线段树#include<bits/stdc++.h>using namespace std;#define M 1000005void Rd(int &res){    char c;res=0;    while(c=getchar(),!isdigit(c));    do res=(res<<3)+(res<<1)+(c^48);    while(c=getchar(),isdigit(c));}struct node{    int l,r,mi,Bcc;//因为有Add这个单词,于是博主用了Bcc。。。}tree[M<<2];int cnt[M];void up(int p){//收集儿子的信息    tree[p].mi=min(tree[2*p].mi-tree[2*p].Bcc,tree[2*p+1].mi-tree[2*p+1].Bcc);}void down(int p){//延迟更新    tree[2*p].Bcc+=tree[p].Bcc;    tree[2*p+1].Bcc+=tree[p].Bcc;    tree[p].Bcc=0;}void build(int l,int r,int p){//建树    tree[p].l=l;tree[p].r=r;tree[p].Bcc=0;    if(l>r)return;    if(l==r){        tree[p].mi=cnt[l];        return;    }int mid=(l+r)/2;    build(l,mid,2*p);build(mid+1,r,2*p+1);    up(p);}bool update(int l,int r,int p,int Bcc){    if(l==tree[p].l&&r==tree[p].r){        tree[p].mi-=tree[p].Bcc+Bcc;//update        tree[2*p].Bcc+=tree[p].Bcc+Bcc;tree[2*p+1].Bcc+=tree[p].Bcc+Bcc;        tree[p].Bcc=0;        return tree[p].mi>=0;//query    }int mid=(tree[p].l+tree[p].r)/2;    bool f;down(p);    if(r<=mid)f=update(l,r,2*p,Bcc);    else if(l>mid)f=update(l,r,2*p+1,Bcc);    else f=update(l,mid,2*p,Bcc)&update(mid+1,r,2*p+1,Bcc);    up(p);//update    return f;//query}int n,m;int main(){    scanf("%d %d",&n,&m);    for(int i=1;i<=n;i++)Rd(cnt[i]);    build(1,n,1);    for(int i=1;i<=m;i++){        int d,s,t;        Rd(d);Rd(s);Rd(t);        if(!update(s,t,1,d)){//不可行            puts("-1");            printf("%d\n",i);            return 0;        }    }puts("0");//可行    return 0;}

第二种解法
二分答案+差分前缀和 O(nlogm)【系数较小】
这题很明显满足二分的性质
于是我们可以二分答案
枚举是哪一个人需要更改订单
既然枚举了修改订单的人
接下来的思路就很直观了
这就跟xiaoC大人题库上的刷漆没什么两样了
差分前缀和随便搞搞就好了

#include<bits/stdc++.h>using namespace std;#define M 1000005void Rd(int &res){    char c;res=0;    while(c=getchar(),!isdigit(c));    do res=(res<<3)+(res<<1)+(c^48);    while(c=getchar(),isdigit(c));}int cnt[M],d[M],s[M],t[M],Add[M];int n,m;bool check(int len){    memset(Add,0,sizeof(Add));//每次check都需要重新计算差分前缀和    for(int i=1;i<=len;i++){Add[s[i]]+=d[i];Add[t[i]+1]-=d[i];}    //第s[i]天起占用教室增多d[i]个,第t[i]+1天起减少d[i]个    int ct=0;    for(int i=1;i<=n;i++){        ct+=Add[i];        if(ct>cnt[i])return false;//该天使用的教室超过限制    }return true;}int main(){    scanf("%d %d",&n,&m);    for(int i=1;i<=n;i++)Rd(cnt[i]);    for(int i=1;i<=m;i++){Rd(d[i]);Rd(s[i]);Rd(t[i]);}    int l=1,r=m,res=0;    while(l<=r){//二分答案        int mid=l+r>>1;        if(check(mid))l=mid+1;//其实是你想多了,它并不用修改订单        else{            res=mid;//确实需要修改订单            r=mid-1;        }    }if(res==0)puts("0");    else printf("-1\n%d\n",res);    return 0;}

第三种方法:(最6的方法)
差分前缀和+贪心 O(n+m)
这个就厉害了,复杂度直接爆掉前两种方法
首先,我们对于全部的订单
求出差分前缀和
贪心的假设所有订单都不需要撤销
当我们扫描到有一天使用的教室数量超出限制时
我们就需要对贪心进行撤销
即把末尾的订单删除
而且我们发现当我们删除一个订单时
前面的那些天数因为使用的教室数量已经小于上限
所以在需要的教室数量只减不增的情况下对前面的订单是没有影响的,
于是乎我们只需要修改当前的差分前缀和的值以及这一天需要的教室数量就可以了
而后最后得到的r的值是最后一个不用修改的订单
于是乎~~
再加1就得到了答案
于是,一个完美的O(n+m)的代码就诞生了
代码清晰简短,很舒胡 ~~~

#include<bits/stdc++.h>using namespace std;#define M 1000005void Rd(int &res){    char c;res=0;    while(c=getchar(),!isdigit(c));    do res=(res<<3)+(res<<1)+(c^48);    while(c=getchar(),isdigit(c));}int cnt[M],d[M],s[M],t[M],Add[M];int n,m;int main(){    scanf("%d %d",&n,&m);    for(int i=1;i<=n;i++)Rd(cnt[i]);    for(int i=1;i<=m;i++){        Rd(d[i]);Rd(s[i]);Rd(t[i]);        Add[s[i]]+=d[i];//差分前缀和        Add[t[i]+1]-=d[i];    }int ct=0,r=m;    for(int l=1;l<=n;l++){        ct+=Add[l];//贪心        while(ct>cnt[l]){            Add[s[r]]-=d[r];//撤销            Add[t[r]+1]+=d[r];            if(l>=s[r]&&l<=t[r])ct-=d[r];//若当前天在r的订单中需要教室,减少当天需要教室数            r--;        }    }if(r==m)puts("0");//全部的订单都无需修改,舒胡~~    else printf("-1\n%d\n",r+1);    return 0;}

C题:
这是史上最难的联赛第三题,
同时也是思考量最大的
首先,我们需要明白的是:
对于某一支军队而言,并不是他已经能覆盖到自己的子树那么它就不移动了
举一个简单的样例进行解释

这里写图片描述
对于这个样例而言,
如果结点3的军队不移动
那么结点2的军队会从节点2走向节点4,耗时3+2=5小时
但是如果3->4,2->3的话
那么时间为max{3+1,1+2}=4小时
于是乎我们知道这题显然不能用dp了
(博主考试dp。。。爆0了。。)
然后我们再仔细观察一下题目
这题中所花的时间最短,不就是行走时间最长的军队时间最短么?
于是乎这种最大值最小问题显而易见可以用二分求解
于是二分所需要的时间判断可不可行
(最大距离可以采用树的直径)
判断的时候很显然,如果一个军队往上走的话,覆盖的路劲是只多不少的
所以我们可以让所有的军队先尽可能的往上走
但是显然首都有点特殊,所以我们的军队都先走它所能走到的最大点或根节点的儿子处
所以此时对于所有的军队只有两种情况:
1.不能再走
2.还能在走到根节点再往下走覆盖其他的子树(显然走到根节点的儿子处为最优)
那些第一种情况的军队,我们可以先将他们能覆盖到的点全部mark
而后考虑第二种情况:
对于第二种情况中,我们经过观察可以发现
对于根节点的某一个子节点而言
他若是有好几支军队驻扎,
肯定要么是剩余时间最小的军队留下,要么就都不留下【第二种情况不存在走不到根节点的点,即已被排除】
我们很容易知道以下几点
1.若该点已经都被mark过,都不留下
2.其实若是时间最小的一支军队走到了根节点之后不能再返回,那么他就没有走出去的必要了
为什么呢?
因为在这样的情况下,
他去覆盖的肯定是距离根结点比他还近的点
但这个时候是需要其他军队来覆盖他自己的,显然这并不划算
还不如其他军队直接去覆盖那个更近的点
于是我们把根节点儿子上的军队转移到了根节点
这个是后就要开始判断可不可行了,
显然是剩余时间最大的军队去距离根节点最远的点
于是我们可以把所有在根节点的军队按剩余时间排序
对所有根节点的字节点按照距离根节点距离的远近排序
之后判断可不可行
(这题数据比较水,走的过程不需要倍增,于是博主懒得敲倍增)

#include<bits/stdc++.h>using namespace std;#define M 50005#define LL long longvoid Rd(int &res){    char c;res=0;    while(c=getchar(),!isdigit(c));    do res=(res<<3)+(res<<1)+(c^48);    while(c=getchar(),isdigit(c));}struct node{    int to,v;    bool operator <(const node &A)const{        return v>A.v;//根据距离从大到小排序    }};vector<node>edge[M];vector<LL>AR[M];//AR[x]代表在点x的结点的集合int A[M],mark[M],fa[M],falen[M],Soncnt[M],dep[M];//mark[x]代表在根节点的军队尚未走下来的时候结点x及其子树是否已经被覆盖//fa[x]代表点x的父亲的id//falen[x]代表点x到父亲的距离//Soncnt[x]代表结点x的儿子的数量//dep[x]代表结点x的深度int id;LL mx;int n,m;bool cmp(LL a,LL b){    return a>b;//根据距离从大到小排序}void dfs(int x,int pre,LL d,int D){//dfs求树的直径的其中一个端点,结点深度,结点的父亲、结点的儿子数以及结点与父亲的距离    dep[x]=D;    if(d>mx){        mx=d;        id=x;    }for(int i=0;i<edge[x].size();i++){        int y=edge[x][i].to;        if(y==pre)continue;        dfs(y,x,d+edge[x][i].v,D+1);        fa[y]=x;falen[y]=edge[x][i].v;Soncnt[x]++;    }}void rdfs(int x,int pre,LL d){//rdfs求树的直径    if(d>mx)mx=d;    for(int i=0;i<edge[x].size();i++){        int y=edge[x][i].to;        if(y==pre)continue;        rdfs(y,x,d+edge[x][i].v);    }}void MARK1(int x){//将x及其子树mark    if(mark[x])return;    mark[x]=1;    for(int i=0;i<edge[x].size();i++){        int y=edge[x][i].to;        if(dep[y]<dep[x])continue;        MARK1(y);    }}void MARK2(int x,int pre){//将所有子节点都被mark的结点mark    int True=0;    for(int i=0;i<edge[x].size();i++){        int y=edge[x][i].to;        if(y==pre)continue;        MARK2(y,x);        True+=mark[y];    }if(True==Soncnt[x]&&Soncnt[x])mark[x]=1;}void init(){//清零    memset(mark,0,sizeof(mark));    AR[1].clear();    for(int k=0;k<edge[1].size();k++){        int x=edge[1][k].to;        AR[x].clear();    }}bool check(LL ti){//判断在ti时间内能否覆盖所有根节点前往叶子结点的路径    init();//初始化数组    for(int i=1;i<=m;i++){        int x=A[i];LL t=0;        while(fa[x]!=1&&t+falen[x]<=ti){//所有军队向根节点方向走            t+=falen[x];            x=fa[x];        }if(fa[x]!=1)MARK1(x);//走不到根节点的子节点        else AR[x].push_back(ti-t);//停在根节点的子节点    }MARK2(1,0);//将已被覆盖的点mark    for(int k=0;k<edge[1].size();k++){        int x=edge[1][k].to;        sort(AR[x].begin(),AR[x].end(),cmp);//对所有的军队排序    }for(int k=0;k<edge[1].size();k++){        int x=edge[1][k].to,sz=AR[x].size();        for(int i=0;i<sz;i++){            if(i==sz-1){//剩余时间最小的军队                if(mark[x]&&AR[x][i]>=falen[x])AR[1].push_back(AR[x][i]-falen[x]);                //该点已经被mark过,能走就走                else {//该点没被mark过                    if(AR[x][i]>=2*falen[x])AR[1].push_back(AR[x][i]-falen[x]);//走的回来就走                    else mark[x]=1;//走不回来不走,覆盖该节点及其子树                }            }else{//剩余时间不为最小的军队                if(AR[x][i]>=falen[x])AR[1].push_back(AR[x][i]-falen[x]);//能走就走                else mark[x]=1;//否则不走,留下来覆盖该节点及其子树            }        }    }sort(AR[1].begin(),AR[1].end(),cmp);//将所有到达根节点的军队按照剩余时间在次排序    int id=0,sz=AR[1].size();    for(int i=0;i<edge[1].size();i++){        int x=edge[1][i].to;        if(mark[x])continue;        if(id==sz)return false;//军队数量不够        if(AR[1][id]<falen[x])return false;//军队剩余时间不足以走到点x覆盖他        id++;    }return true;//ti时间可以覆盖所有根节点到叶结点的路径}int main(){    scanf("%d",&n);    for(int i=1;i<n;i++){        int a,b,c;        Rd(a);Rd(b);Rd(c);        node tmp;tmp.to=b;tmp.v=c;        edge[a].push_back(tmp);        tmp.to=a;        edge[b].push_back(tmp);    }sort(edge[1].begin(),edge[1].end());//将根节点的子节点按照距离大小排序    dfs(1,0,0,1);    mx=0;rdfs(id,0,0);    scanf("%d",&m);    for(int i=1;i<=m;i++)Rd(A[i]);    LL l=0,r=mx,res=-1;    while(l<=r){//二分答案        LL mid=l+r>>1;        if(check(mid)){            res=mid;            r=mid-1;        }else l=mid+1;    }cout<<res<<endl;    return 0;}

于是200分飘过~~~~
两天相加430瞬间爆炸。。。。

5 1
原创粉丝点击