noip 2015 day2解题报告

来源:互联网 发布:淘宝店铺号申请 编辑:程序博客网 时间:2024/05/16 18:07

写在前面

因为本解题报告采用了许多大佬的题解与代码,所以记录不过来便设置为转载,如果侵犯了大佬的权益请联系我(也就是转侵删)

跳石头

  1. 题目描述

       这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终 点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达 终点   为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。
  2. 思路
    二分答案即可

代码:#include <cstdio>#include <cstring>using namespace std;const int maxn=50005;int a[maxn],n,m,ans;bool check(int x){    int sum,last;    sum=0;last=0;    for (int i=1;i<=n;i++)    {        if (a[i]-last<x) {  sum+=1;continue;}//如果小于就移走当前的石头         last=a[i];    }    if (sum>m) return 0;    return 1;}int main(){    int L,R,mid;    scanf("%d%d%d\n",&L,&n,&m);    for (int i=1;i<=n;i++) scanf("%d\n",&a[i]);    n+=1;    a[n]=L;R=L;    ans=L=0;    while (L<=R)    {           mid=L+(R-L)/2;        if (check(mid))        {          ans=mid;          L=mid+1;          }        else R=mid-1;    }//二分过程     printf("%d",ans);    return 0;}  

子串

 1. 思路     首先讲一种最简单的DP吧(也是我最容易看懂的),不过在洛谷上似乎有更优解     我们令f[i][j][k][0/1]表示A串用了前i个字符,B串已覆盖前j个字符,目前为止已经选了k个子串,最后的0/1表示A串的这个字符选了没有(0没选,1选了)。     为了得出状态转移方程,我们分情况讨论:先看f[i][j][k][1](当前位选了),显然当且仅当a[i]=b[j]的时候它才有意义,否则f[i][j][k][1]=0。到这个状态有三种方法:上一位没有选,新开一个子串 上一位选了,延续这个子串上一位选了,但是仍然新开一个子串因此,我们有f[i][j][k][1]=f[i-1][j-1][k-1][0]+f[i-1][j-1][k][1]+f[i-1][j-1][k-1][1]。    状态转移方程中的三项分别对应上述三种情况。注意,因为我们规定了A的这一位必须选(因为状态的最后一维是1),所以所有前驱状态一定是f[i-1][j-1][…][…]。然后讨论另一种情况:这个字符不选。这个比较简单,到这个状态有两种方法:上一位没有选,现在仍然不选上一位选了,结束这个子串 因此,我们有f[i][j][k][0]=f[i-1][j][k][0]+f[i-1][j][k][1]。合起来就是f[i][j][k][1]=f[i-1][j-1][k-1][0]+f[i-1][j-1][k][1]+f[i-1][j-1][k-1][1](a[i]=b[j])f[i][j][k][1]=0(a[i]!=b[j])f[i][j][k][0]=f[i-1][j][k][0]+f[i-1][j][k][1]状态转移方程有了,边界也容易确定:f[0][0][0][0]=1。至于最终答案,显然是f[n][m][k][0]+f[n][m][k][1]然后用滚动数组压掉一维就可以
代码:#include <iostream>#include <cstdio>#include <cstring>#include <algorithm>using namespace std;const int N=1005,M=205,K=205,MOD=1000000007;int n,m,p;char a[N],b[M];int f[2][M][K][2],pre=1,now=0,ans=0;void dp(){    //因为我们发现i时的值只由i-1传递过来,所以只需要用pre,now来定义i-1,i;     f[pre][0][0][0]=f[now][0][0][0]=1;//初始化     for(int i=1;i<=n;i++,swap(now,pre))//把现在的now数组存i,pre为i-1;         for(int j=1;j<=m;j++)            for(int k=1;k<=p;k++){                if(a[i]==b[j])                     f[now][j][k][1]=(f[pre][j-1][k-1][0]+f[pre][j-1][k][1])%MOD;                else                     f[now][j][k][1]=0;                    f[now][j][k][0]=(f[pre][j][k][0]+f[now][j][k][1])%MOD;//DP主体;             }}int main() {    scanf("%d%d%d%s%s",&n,&m,&p,a+1,b+1);    dp();    cout<<f[pre][m][p][0];     return 0;}

运输计划

  1. 题意
    给你一棵树,有很多条路径,让你把一条边的边权设为0,使路径的最大值最小
  2. 思路
    lca+二分
    大概思路就是首先二分一个答案(时间),然后对各个计划进行遍历求值,记录时间大于答案的路线并维护最大差值sum,然后寻找这几条路线里有没有公共边且使最长公共边长度变为0后是否合法,不合法就把时间扩大,合法就缩小
    (思路确实简单)
/*算法:tarjan + 二分思路:先找lca,可以在dfs的时候就用tarjan直接算出,然后找出s -> t的路程,即 q[i].dis = deep[q[i].from] + deep[q[i].to] - 2* deep[q[i].lca];继续进行二分,对于路程大于假定答案的就cnt++,不然就break,因为提前排过序,最后发现只能对于cnt==num[i]的点进行虫洞操作,最后如果大于就继续缩小,反之增大上限。c++代码如下:#include<iostream>#include<cstring>#include<algorithm>#include<cstdio>#include<queue>#include<stack>using namespace std;const int N = 3e5 + 1;int head[N],f[N],deep[N],v[N],num[N],headm[N],tot,n,m;inline void read(int&x){    x=0;char c;int sign=1;    do{ c=getchar();if(c=='-') sign=-1;}while(c<'0'||c>'9');    do{ x=x*10+c-'0';c=getchar();}while(c<= '9'&&c>='0');    x *= sign;}//读入优化 struct node{    int to,next,val;}edge[N*4];//结构体 struct str{    int from,to,lca,dis;}q[N];void add(int head[],int a,int b,int val){    edge[tot].next=head[a];    edge[tot].to=b;    edge[tot].val=val;    head[a]=tot++;}inline int get_fa(int x) { return x==f[x]?x:f[x]=get_fa(f[x]);}//找爸爸(你的就不用找了,因为就是我[滑稽]) void dfs(int u,int fa){    f[u]=u;    for(int i=headm[u];~i/*这表示i=-1就结束因为初始化时数组定的是-1(第87行)*/;i=edge[i].next)    {        node &e=edge[i];//其实觉得这一段没这么必要,不过简洁了些;         if(f[q[e.to].from]&&u==q[e.to].to) q[e.to].lca=get_fa(q[e.to].from);        if(f[q[e.to].to]&&u==q[e.to].from) q[e.to].lca=get_fa(q[e.to].to);//求LCA     }    for(int i=head[u];~i;i=edge[i].next)    {        node&e=edge[i];        if(e.to==fa) continue;        deep[e.to]=deep[u]+e.val;//更新深度         dfs(e.to,u);        v[e.to]=e.val;    }    f[u]=fa;//确立父子关系 }const bool cmp(str a,str b){ return a.dis>b.dis;}void find(int u ,int fa){    for(int i=head[u];~i;i=edge[i].next)    {        node&e=edge[i];        if(e.to==fa)continue;        find(e.to,u);        num[u]+=num[e.to];    }}//标记上推; bool check(int x){    int cnt=0,dec=0;    memset(num,0,sizeof(num));//清零标记;     for(int i=1;i<=m;i++)        if(q[i].dis>x){cnt++;dec=max(dec,q[i].dis-x);num[q[i].from]++;num[q[i].to]++;num[q[i].lca]-=2;}        //处理非法路,因为待会标记会上推所以最近公共祖先先-=2,这样等下推的时候最近公共祖先的数组值仍会为0,就不会影响结果了;         else break;    find(1,1);    for(int i=1;i<=n;i++)        if(cnt==num[i]&&v[i]>=dec)return 1;//如果删去的这个路的值比最大差值大就return 1否则return 0return 0;}int main(){    memset(head,-1,sizeof(head));memset(headm,0,sizeof(headm));//初始化数组;     read(n);read(m);    int u,v,val;    for(int i=1;i<n;i++)    {        read(u);read(v);read(val);        add(head,u,v,val);add(head,v,u,val);//边数组     }    for(int i=1;i<=m;i++)        read(q[i].from),read(q[i].to),add(headm,q[i].from,i,0),add(headm,q[i].to,i,0);//询问数组;     dfs(1,1);//也就是所谓的trajan;     int l=0,r=0,num=0;    for(int i=1;i<=m;i++){q[i].dis=deep[q[i].from]+deep[q[i].to]-2*deep[q[i].lca];r=max(q[i].dis,r);}//求最短路;     sort(q+1,q+1+m,cmp);//将路径从大到小排序便于进行     while(l<=r)    {        int mid=(l+r)>> 1;        if(check(mid))num=mid,r=mid-1;        else l=mid+1;     }//二分主体;     cout<<num<<endl;    return 0;}

这里算是个人的一些感想???

1.这几道题目可是折磨了我好久(主要第三道题,也体会到了大佬们的超神)
2.找题解的过程里发现好多大佬的网页是真的可爱(雾?),我也最喜欢去看这样的题解,因为看起来不是冷冰冰的,运用到与人交流上或许也是如此。
3.发现大佬的思路跟我完全是不同的,比喻为走路的话我或许是想着怎么走到,而大佬却考虑
的是怎么快点走到,所以有好多优化啊那种东西完全看不懂,这或许也就级别的差距吧,我也要好好的加强自己了,至少要追到能看到大佬的背影(%%%)
这里写图片描述

原创粉丝点击