UVa的几道水题题解

And Then There Was One, Japan 2007,LA 3882 题解

Vjudge传送门


题意:

n 个数排成一圈,第一次删除m,以后没 k 个数删除一次,求最后一个被删除的数
数据范围:多组数据,2<=n<=100001<=k<=100001<=m<=n


题解:

显然我们已经不能模拟了,因为数据规模实在是太大啦,已经不再是猴子选大王靠模拟就可以搞出来的了。然而怎么做呢,我们可以用递推搞出来嘛?仿佛是可以的,因为如果是 n 个猴子选大王,第一个猴子被踢出去之后,剩下不就是n1个猴子嘛?所以我们完全可以重新编个号,从 n 个猴子中被踢出去的那只猴子之后重新开始编12345...那么 f[n1] 的答案不是可以直接 +k 就是原来的编号吗?因为在重新编号的时候所有的编号都被 k 了,所以我们就可以愉快地递推了,注意% n 即可。


代码:

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int n,k,m,f[10000+1000];
int main(){
    while(scanf("%d%d%d",&n,&k,&m)!=EOF&&n&&k&&m){
        f[1]=0;
        for(register int i=2;i<=n;i++)f[i]=(f[i-1]+k)%i;
        int ans=(m-k+1+f[n])%n;
        if(ans<=0) ans+=n;
        printf("%d\n",ans);
    }
    return 0;
}

UVaLive 3905 Meteor ,Seoul 2007 题解

Vjudge传送门


题意:

给你n个点,然后这n个点会按照给出的x轴速度,y轴速度运动,我们需要求得是,现在给你一个固定的相框,输出n个点出现在这个固定的相框里最多的时候一共有n个中的多少个点


题解:

如果直接正面去求这个,会发现很难算,所以我们需要转化思路,既然是求n个点中某一时刻尽量出现更多的点在这个框里,那么我们的想法是逆向思维,先把n个点出现的时刻先计算出来,然后再记录到一个结构体里面,即一个区间l,r,表示这个点在l,r这段时间内出现次数最多,于是我们可以得出一个算法,求解在某一时刻,重叠的区间数最多,输出区间最多是多少个,显然我们可以把左端点和右端点当做两个事件,也就是说,对所有事件排了序之后我们从左至右扫描(因为时间是向量,只能线性地流逝不能纵向叠加[当然对于蛤来说除外]),如果扫到一个左端点,那么说明新进来了一个区间,如果扫到一个右端点,那么说明出去了一个区间,注意一点有点坑,也就是这道题在框上的点是不算的,因此我们需要把右端点的事件的优先级设置为比左端点大,也就是说倘若是同一个时间,一个是左端点,一个是右端点,那么右端点这个时间的优先级高于左端点,这样一来会先扫描到右端点,再扫描到左端点,也就是先出再进,才能保证答案的正确性,至此我们就已经得到了完整的算法,在计算一个点在框内的时间的时候有一个小技巧,也就是先计算x坐标位于相框区间的时间,再计算y坐标位于相框区间的时间,这样两个时间的交集显然就是x坐标位于相框区间,y坐标位于相框区间的时刻,这道题貌似没有eps的问题,但是其实可以转化成整数,貌似是同时乘以一个什么公倍数。


代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
void update(int x,int a,int w,double& L,double& R){
    if(a==0){
        if(x<=0||x>=w) R=L-1;
    }else if(a>0){
        L=max(L,-(double)x/a);
        R=min(R,(double)(w-x)/a);
    }else{
        L=max(L,(double)(w-x)/a);
        R=min(R,-(double)x/a);
    }
}
const int MAXN=100000+10;
struct Event{
    double x;int type;
    bool operator < (const Event& a) const{return x<a.x||(x==a.x&&type>a.type);}
}event[MAXN*2];
int main(){ 
    int T;
    scanf("%d",&T);
    while(T--){
        int w,h,n,tail=0;
        scanf("%d%d%d",&w,&h,&n);
        for(register int i=1;i<=n;i++){
            int x,y,a,b;
            scanf("%d%d%d%d",&x,&y,&a,&b);
            double L=0,R=1e9;
            update(x,a,w,L,R);
            update(y,b,h,L,R);
            if(R>L){
                event[++tail]=(Event){L,0};
                event[++tail]=(Event){R,1};
            }
        }
        sort(event+1,event+tail+1);
        int cnt=0,ans=0;
        for(register int i=1;i<=tail;i++){
            if(event[i].type==0) ans=max(ans,++cnt);
            else                 cnt--;
        }
        printf("%d\n",ans);
    }
    return 0;
}

Jurassic Remains,NEERC 2003,LA 2965 题解


Vjudge传送门


题意:

大概的题意是指现在有n个由大写字母组成的字符串,选择尽量多的穿,是的每个大写字母都出现了偶数次


题解:

最开始我想的就是bitset暴力,说不定还能卡过,因为(1<=n<=24),所以我们直接bitset说不定也是可以的//直接穷举应该就可以了233.复杂度是224卡点常数。我们还有一个比较优美的方法,是把前一半的序列的所有可能搞出来,然后一个集合对应的尽量多的字符串,于是我们用map来维护一下,然后对于后半部分,我们边计算边查找,然后ans不断地取max就可以了,写一个求二进制位有多少个1的函数。时间复杂度降到了 2n/2log2n ,更加优美,这种思路是密码学中很著名的中途相遇攻击法(Meet-in-the-Middle Attack)


代码:

#include<cstdio>
#include<iostream>
#include<cstring>
#include<map>
using namespace std;
map<int,int>mp;
const int MAXN=100+10;
int bitcount(int x){
    int cnt=0;
    while(x){if(x&1!=0) cnt++; x>>=1;}
    return cnt;
}
int main(){
    int n,A[MAXN];
    char s[1000];
    while(scanf("%d",&n)==1&&n){
        for(register int i=0;i<=n-1;i++){
            scanf("%s",s);
            A[i]=0;
            for(register int j=0;s[j]!='\0';j++) A[i]^=(1<<(s[j]-'A'));
        }
        mp.clear();
        int n1=n/2,n2=n-n1;
        for(register int i=0;i<(1<<n1);i++){
            int x=0;//表示一种集合的异或和 
            for(register int j=0;j<n1;j++) if(i&(1<<j)) x^=A[j];
            if(!mp.count(x)||bitcount(mp[x])<bitcount(i)) mp[x]=i;
        }
        int ans=0;
        for(register int i=0;i<(1<<n2);i++){
            int x=0;
            for(register int j=0;j<n2;j++) if(i&(1<<j))x^=A[n1+j];
            if(mp.count(x)&&bitcount(ans)<bitcount(mp[x])+bitcount(i)) ans=(i<<n1)^mp[x];
        }
        printf("%d\n",bitcount(ans));
        for(register int i=0;i<n;i++) if(ans&(1<<i)) printf("%d ",i+1);
        printf("\n");
    }
    return 0;
} 

Prince ans Princess,UVa 10635题解

Vjudge传送门


题意:

现在有两条长度为p+1和q+1的序列,每个序列的元素都各不相同,都是 1 n2的数,求两个序列的LCS,p,q的范围是 n2 内,n的范围是250,多组数据


题解:

LCS我貌似只会pq的写法,显然是要挂的(尤其是ccf老年机),所以我们想一下其他解法来解LCS,显然这道题有一个性质,也就是无论是第一条序列还是第二条序列,他们的元素都是互异的,因此我们的思路是先给A数组重新编号,从1开始,一直编到A的长度,对于B数组,如果这个位置的数在A中出现过,那么就应该是这个数在A中出现的位置的值,如果没有出现过就是0,然后我们求一下B的LIS就可以了,LIS貌似我们是可以nlogn做的,所以这道题就迎刃而解了。
其实刚才说的这个道理是很清楚的,因为我们把所有的B数组都变成了A数组中该数字出现的位置,那么如果找出一个连续递增的B数组,一定可以找到A和B的LCS为B的LIS所表示的这个LCS,也即B中找到的序列,在A中的位置要递增,显然吧如果在B中位置是递增的但是在A中位置不递增,怎么LCS?所以这道题就这么转化啦,非常愉快


代码:


#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN=250*250+10;
const int INF=1000000000;
int a[MAXN],b[MAXN],c[MAXN],num[MAXN],l[MAXN],g[MAXN],T,tail;
int main(){
    scanf("%d",&T);
    for(register int kase=1;kase<=T;kase++){
        int N,p,q,x;
        scanf("%d%d%d",&N,&p,&q);
        memset(c,0,sizeof(c));
        for(register int i=1;i<=p+1;i++) scanf("%d",&a[i]),c[a[i]]=i;
        for(register int i=1;i<=q+1;i++){
            scanf("%d",&b[i]),b[i]=c[b[i]];
            if(b[i]!=0) num[++tail]=b[i];
        }
        int ans=0;
        for(register int i=1;i<=tail;i++) g[i]=INF;
        for(register int i=1;i<=tail;i++){
            int k=lower_bound(g+1,g+tail+1,num[i])-g;
            l[i]=k;g[k]=num[i];
            ans=max(ans,l[i]);
        }
        printf("Case %d: %d\n",kase,ans);
    }
    return 0;
}
/*
1 
3 6 7
1 7 5 4 8 3 9
1 4 3 5 6 2 8 9
*/

Game of Sum,UVa 10891题解

题意:

有一个长度为n的序列,然后这个序列的元素可正可负,现在要A和B轮流取数,每次可以取左边或者右边的任意多个数,当然你可以在某一次把这个序列直接取完,A先取,求A的得分减去B的得分后的结果


题解:

显然这道题如果问的不是是A取的分减去B取的分而是A取的分加上B取的分的话,会好做得多(这不是废话嘛)…
也就是说,A和B取的分的和是一定的,就看谁取得多,谁取的少,但是总和是一定的,所以到这里我们大概已经想到了转移的方法,另一个dp[i][j]表示i到j这个区间,先手能够获得的最大收益,那么显然我们可以初始化dp数组为dp[i][i]=a[i],然后我们会发现每次转移的时候,我们可以枚举一下i和j中间的点,使得前一段先手的人拿然后留后一段给另一个人先手,或者后一段先手的人拿留前一段给另一个人先手,所以我们可以推出

dp[i][j]=sum[i][j]min(dp[i][i],dp[i][i+1]...dp[i][j1],dp[j][j],dp[j1][j]...dp[i+1][j],0)

然后我们每次枚举转移就可以了,但是据说还有一种可以优化到 n2 的方法,就是记一个f数组和一个g数组,分别表示dp数组前一段区间的最小值和dp数组后一段区间的最小值,然后转移就变成了O(1)的,不过貌似不是很必要,但是很优美是承认的233


代码:

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN=100+10;
int n,b[MAXN],a[MAXN],dp[MAXN][MAXN];
//4
//4 -10 -20 7 
int main(){
    while(scanf("%d",&n)==1&&n){
        for(int i=1;i<=105;i++){
            for(int j=1;j<=105;j++){
                dp[i][j]=-100000000;
            }
        }
        b[0]=0;
        for(register int i=1;i<=n;i++){scanf("%d",&a[i]);dp[i][i]=a[i];b[i]=b[i-1]+a[i];}
        for(register int len=2;len<=n;len++){
            for(register int i=1;i<=n-len+1;i++){
                int j=i+len-1;                                          
                for(register int k=i+1;k<=j;k++){dp[i][j]=max(dp[i][j],b[j]-b[i-1]-dp[k][j]);}
                dp[i][j]=max(dp[i][j],b[j]-b[i-1]);
                for(register int k=i;k<j;k++)dp[i][j]=max(dp[i][j],b[j]-b[i-1]-dp[i][k]);
            }
        }
        printf("%d\n",2*dp[1][n]-b[n]);
    }
    return 0;
}

Calculator Conundrum,UVa 11549题解


题意&题解:

有一个神奇的计算器,然后每次计算之后只显示前n位,比如n为5的时候2147483647显示21474,然后我们只需要处理一下long long然后转字符串然后再转回来就行了,然后突然发现可以直接除啊…我是不是傻…竟然copy了刘汝佳的代码


代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int buf[10],T,n,k;
int next(int n,int k){
    if(!k) return 0;
    long long k2=(long long)k*k;
    int L=0;
    while(k2>0){buf[L++]=k2%10;k2/=10;}
    if(n>L) n=L;
    int ans=0;
    for(register int i=0;i<n;i++) ans=ans*10+buf[--L];
    return ans;
}
int main(){
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&k);
        int ans=k;
        int k1=k,k2=k;
        do{
            k1=next(n,k1);
            k2=next(n,k2);if(k2>ans) ans=k2;
            k2=next(n,k2);if(k2>ans) ans=k2;
        }while(k1!=k2);
        printf("%d\n",ans);
    }
    return 0;
}

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值