2014年百度之星资格赛(解题报告)

题目链接

资格赛的题目难度也不高,只是被第三题整得快哭了而已 = =

先吐槽下那天的遭遇,想看题解的可以选择跳过。。。

那天开始的时候,A和D都1Y掉了,然后在那里纠结B和C。C那会儿没思路,B写了一发WA了。然后就去上课。上课的时候想到了算法。下课后回来一写B和C都给T了。。。B改了改就过了。

C看了下,尼玛我加了输入挂都T?看了下输出量也挺大,于是把输出的挂也加了上去,照样T。我无语了,难道我挂写丑了?再三看没问题哩。唉,改成普通的scanf和printf,一交,WA。呃,难道真是我输入挂的问题?呃,不管了。C我的写法一开始是采用按位逐步二分,没用Trie。WA了半天不爽,跑去看提问那边。纳尼?数据有错?!呃,有改正?呃,不对,这是在我上课期间的事情了。这么说数据没问题?好吧,我继续盯着我的代码。

纠缠了很久,尝试各种修改,都是WA,WA,WA。这时,我看了下过的人的情况,我WA掉的代码是4000+K的内存,绝大多数过的人的内存都是20000+K,400000+K之类,总之就是比我大很多。然后我就开始怀疑是我算法错,可是我又实在想不出他们开什么数据结构要这么多!后来,不知道怎么的就突然想到Trie,改成Trie一交,还是WA,可是内存跟上大牛们的脚步了。难道大牛都是写Trie?

可是还是无尽WA,再跑去看提问。有位中大的说数据还是有问题,漏数据?我写了一个如果后面少于m个数的就除以0,结果真的引来RE!可是数据少了要怎么办?不处理?我加了个break,交,还是WA!赋值为0?WA!最后出现的值代替?AC!一看,连T带WA一共挂了50发才过。。。这时我又拿回前面4000+K的代码,针对性地修改了,照样能AC。。。我勒个去,就因为这个纠结了我4个小时。。。

然后回头想了下,由于输入的数据组数是读个t确定的,所以我没有做EOF判断,输入挂就因为这样读不到字符陷入死循环。

到今晚重新回去看发现我的罚时又变少了?看样子是又修复了数据rejudge多一次?

唉,不管了。。。


HDU 4823 —— Energy Conversion

A题,其实题意很简单,就是问M能否通过有限次的(M-V)*K迭代,使得M大于等于N。

如果一开始M>=N了,直接输出0。

否则转到下面。

因为数字都是正整数,而迭代第一步是M-V,M先减小,然后乘以K。如果M最终能大于等于N,M肯定是不断增大的。

所以先判断(M-V)*K是否能大于M,不满足则输出-1。

否则就说明能达到。直接暴力求就行啦,因为能够增大的话K至少是2,N不过10^8,K就算是2,迭代次数也不超过32次,所以不用担心超时的。

唯一注意的是,计算过程M会爆int,要用long long。

#include<cstdio>
#define LL long long
int t;
LL n, m, v, k;
int main(){
    scanf("%d", &t);
    while(t--){
        scanf("%I64d %I64d %I64d %I64d", &n, &m, &v, &k);
        if(m>=n){
            puts("0");
            continue;
        }
        if(m<v){
            puts("-1");
            continue;
        }
        LL tmp = (m-v)*k;
        if(tmp<=m){
            puts("-1");
            continue;
        }
        int cnt = 1;
        while(tmp<n){
            tmp = (tmp-v)*k;
            cnt++;
        }
        printf("%d\n", cnt);
    }
    return 0;
}

HDU 4826 —— Labyrinth

D题,DP。题意就不用说了吧。

一开始看到移动,“向上向下向右”这里,习惯性地以为写了4个方向,觉得写搜索都痛苦。结果被人很快KO掉回来一看才发现,没有“向左”!没有向左问题马上变得简单了,可以按照从左到右DP。

dp[i][j]表示在第i行第j列的最优值,考虑从它向右移动并且停留到第j+1列的第k格,就是从这个格可以转移出去的状态,然后计算过程每个状态取最大值即可。

这里可以先用S[i][j],表示第j列第1行到第i行的和,那么转移时就不用再去一个个累加了,直接利用前缀和相减即可。

#include<cstdio>
#include<cstring>
int t, ct, n, m, x, k, i, j, a[101][101], s[101][101], dp[101][101];
bool f[101][101];
int main(){
    scanf("%d", &t);
    for(ct=1; ct<=t; ct++){
        scanf("%d %d", &n, &m);
        memset(f,0,sizeof(f));
        memset(s,0,sizeof(s));
        for(i=1; i<=n; i++){
            for(j=1; j<=m; j++){
                scanf("%d", &a[i][j]);
                s[i][j] = s[i-1][j]+a[i][j];
            }
            dp[i][1] = s[i][1];
            f[i][1] = 1;
        }
        for(j=2; j<=m; j++){
            for(i=1; i<=n; i++){
                for(k=1; k<=n; k++){
                    if(i<k) x = s[k][j]-s[i-1][j];
                    else    x = s[i][j]-s[k-1][j];
                    x+=dp[i][j-1];
                    if(!f[k][j] || x>dp[k][j]){
                        dp[k][j]=x;
                        f[k][j]=1;
                    }
                }
            }
        }
        printf("Case #%d:\n%d\n", ct, dp[1][m]);
    }
    return 0;
}


HDU 4824 —— Disk Schedule
B题,我还是写成DP+BFS,据说是双调最短路?

首先,由于跳转一次磁道的消耗比在磁道上旋转一周还要大,所以尽量避免跳转磁道。那么最少的跳转方式,就是从0跳转到需要访问的最顶上的磁道,再从那里跳回来。记最高那个为top,那么磁道间跳转就是top*400*2=top*800。然后每次读取耗时10,所以固有的消耗是top*800+n*10。

剩下的就是旋转的消耗。由于我们在磁道间来回扫了一次。所以每个磁道上的读取任务,要么去的时候完成,要么回来的时候完成。如果直接记录去再回是比较麻烦的,可以考虑把回来看成逆向的去。从0磁道开始往top扫,dp[i][j]表示去的路线到达了i,逆向的路线到达了j,i和j都是扇区编号。那么当前磁道的读取,假设在扇区x,要么由i转移过来,要么由j过来,分别拿去更新dp[x][j] 和 dp[i][x]即可。

到达最后的时候记得top需要同时从i和j转移,所以两边都要计算。

另外,转移的时候,因为每次固定从上一个磁道转移过来,假设上一个磁道的任务在y扇区,那么可以确定上一个状态要么是dp[i][y],,要么是dp[y][j],所以可以固定y枚举其它扇区。

直接这样写的后果是TLE。检查了下发现其实还是有很多无效状态的。改成BFS的方式就AC了,但是,感觉耗时还是挺大的。

#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int n, t, ct, i, j, tmp, ans, b[1000];
bool f;
int dp[2][360][360];
bool in[360][360];
struct Point{
    int l, r, id;
}p;
int dis(int x, int y){
    int A, B;
    if(x<y){
        A = y-x;
        B = 360+x-y;
    }
    else{
        A = x-y;
        B = 360+y-x;
    }
    return A>B?B:A;
}
int main(){
    scanf("%d", &t);
    while(t--){
        int x, y;
        scanf("%d", &n);
        int top=0, tmp;
        for(i=0; i<n; i++){
            scanf("%d %d", &x, b+i);
            if(x>top)   top=x;
        }
        f = 0;
        i = 0;
        x = 0;
        queue<Point> Q;
        Q.push((Point){0,0});
        Q.push((Point){-1,-1});
        dp[0][0][0]=0;
        memset(in,0,sizeof(in));
        in[0][0]=1;
        while(!Q.empty()){
            p = Q.front(); Q.pop();
            if(p.l==-1){
                if(Q.empty())   break;
                f^=1;
                x=b[i]; i++;
                if(i>=n)    break;
                memset(in,0,sizeof(in));
                p = Q.front(); Q.pop();
                Q.push((Point){-1,-1});
            }
            tmp = dp[f][p.l][p.r] + dis(p.l, b[i]);
            if(!in[b[i]][p.r]){
                dp[f^1][b[i]][p.r] = tmp;
                in[b[i]][p.r]=1;
                Q.push((Point){b[i],p.r});
            }
            else if(tmp<dp[f^1][b[i]][p.r]){
                dp[f^1][b[i]][p.r]=tmp;
            }
            tmp = dp[f][p.l][p.r] + dis(p.r, b[i]);
            if(!in[p.l][b[i]]){
                dp[f^1][p.l][b[i]] = tmp;
                in[p.l][b[i]]=1;
                Q.push((Point){p.l,b[i]});
            }
            else if(tmp<dp[f^1][p.l][b[i]]){
                dp[f^1][p.l][b[i]] = tmp;
            }
        }
        ans = -1;
        for(i=0; i<360; i++){
            if(in[i][x]){
                tmp = dp[f][i][x]+dis(i,x);
                if(ans==-1 || tmp<ans)  ans=tmp;
            }
            if(in[x][i]){
                tmp = dp[f][x][i]+dis(i,x);
                if(ans==-1 || tmp<ans)  ans=tmp;
            }
        }
        ans += n*10 + top*800;
        printf("%d\n",ans);
    }
    return 0;
}

HDU 4825 —— Xor Sum

C题,唉,这真是个噩梦。。。

无论是直接按位二分,还是Trie,其实本质思想都是二分,因为Trie每个结点最多两个分支。只是内存使用量不同。

呃,我又想吐槽一下,不是说是集合吗?集合元素不是没有重复吗?为什么第二个样例就有两个6???好吧,这个对解题没有影响。

由于庞大的数据量,直接暴力是不可行的。

既然异或是二进制的运算,我们就往二进制想。由于答案要尽量大,所以高位越早出现1,异或的结果肯定就越大。比如第一个样例,三个数字的二进制分别是011,100,101,对于1这个查询,1的二进制写成001。这里由于数字前面都是0,对运算没有影响,所以不用管了。

因为我们要尽量让前面得到1,所以对于001,左边的0就应该找个1给它,那么011就被淘汰了,剩下100,101,这两个数字由于左边的1被匹配掉了,只需要考虑00和01。

接下来一位是0,候选的都是0,所以不用管,下一位0和1,由于我们要和1匹配,所以就找到0,答案4就出来了。

用Trie实现的话比较简单,就是先把数字按照二进制拆开,然后就像插入字符串那样将01串插进去,叶子结点当然就是对应当前路径的数字。然后对于每个查询,按照二进制位找。假设当前结点u,我们就看看与当前查询相反的位置是否有结点,有就过去,否则就另一个方向。最后输出叶子结点上的值就行了。


另外一种是利用二分查找不断缩小解的范围,这个需要对数据先排序,排完序再将每个数字的二进制存储起来。

由于已经排序完了,一个明显的性质是,一段连续的数字,假设它们的二进制前面n位是相同的,那么在第n+1位,肯定是前部分的数字是0,后部分是1,极限一点是全部都是0或1。然后无论我们选择0还是1,我们可以把它们的范围缩小,那么剩下的数字还是满足前面的性质。

这样一来,我们对于每个查询,假设当前是二进制第i位,解的区间是[L,R],如果这个区间上的第i位是一样的,没得选,我们就继续看下一位,不然呢,就先二分查找出区间上最后一个0,假设是第K个,那么第K+1个的二进制位就是1了。然后我们根据查询的第i位是0还是1,缩小解的区间。

当扫完最后一位时,剩下的区间[L,R],有可能这个区间长度大于1,但是这些数字都是相等,因为它们的01串都是相同的,那么L对应的数字就是答案。

#include<cstdio>
#include<algorithm>
using namespace std;
#define N 100010
int t, ct, n, m, l, r, i, j, ans, low, top, mid;
bool f[N][34], x[34];
long long a[N], b;
int main(){
    scanf("%d", &t);
    for(ct=1; ct<=t; ct++){
        scanf("%d %d", &n, &m);
        for(i=0; i<n; i++){
            scanf("%I64d", &a[i]);
        }
        sort(a, a+n);
        for(i=0; i<n; i++){
            b = a[i];
            for(j=0; j<34; j++){
                if(b%2==1)  f[i][j]=1;
                else    f[i][j]=0;
                b/=2;
            }
        }
        printf("Case #%d:\n", ct);
        long long y;
        while(m--){
            scanf("%I64d", &b);
            /*
            比赛的时候,这句不加会WA,现在没问题了
            if(b<=0)    b=y;
            y = b;
            */
            for(i=0; i<34; i++){
                if(b%2==1)  x[i]=1;
                else    x[i]=0;
                b/=2;
            }
            l=0; r=n-1;
            for(i=33; i>=0; i--){
                if(f[l][i]==f[r][i])    continue;//区间上这一位的二进制都一样
                ans = l;
                low=l; top=r;
                while(low<=top){
                    mid = (low+top)/2;
                    if(f[mid][i])   top=mid-1;
                    else{
                        if(mid>ans) ans=mid;
                        low=mid+1;
                    }
                }
                if(x[i])    r=ans;
                else    l=ans+1;
            }
            printf("%I64d\n", a[l]);
        }
    }
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值