【20150915】NOIP模拟套题02 day2 题解 & 总结

前言

  隔了3天做的day2……对我而言难度爆炸,最后只有20分也是给跪了……很多人在T3上刷了不少分,我咋就没想到呢?

T1_WYF的盒子

  题目大意:给出 k, n, m, p ,其中 90% 的数据满足 k2000, n,m,p1012 ,另外 10% 的数据满足 k,n,m,p1012, nm5000 。求 (ni=mik)modp

  题目描述相当简单,但是如果不懂一些数学知识,要拿分那简直是难上加难。比赛时我只会后 10% 数据的做法,至于剩余的数据我想了一个手推公式的做法,结果前面的数据却只有10分,加起来20分,成了我唯一的分数……
  其实我的想法是没错的,只不过因为对基础数学知识的运用不熟练,导致我没想到更好的优化方案。
  设 Sumxk=xi=1ik 。我们现在要求公式。考虑使用累加法求解:

(i+1)k+1ik+1=j=0k+1(Cjk+1ij)ik+1=j=0k1(Cjk+1ij)+(k+1)ik

  我们求 xi=1[(i+1)k+1ik+1] ,可得:
(x+1)k+11k+1=i=1xj=0k1(Cjk+1ij)+(k+1)i=1xik=j=0k1(Cjk+1Sumxj)+(k+1)Sumxk

  从上式中我们发现:如果 x=n ,通过对上式变形我们就可以求得 Sumnk 。而题目所要求的正是 SumnkSumm1k
Sumxk=(x+1)k+11k1j=0(Cjk+1Sumxj)k+1

  分析求上式的时间复杂度。我们发现其中包含了 Sumxj ,因为 0j<k ,这启示我们可以先求 Sumx0 ,再求 Sumx1 ……一直递进求到 Sumxk 。加上求组合数和快速幂的复杂度,总的时间复杂度为 O(k2+klogk) ,可以通过前 90% 的数据。而剩下的 10% 我们直接暴力求快速幂即可。至此本题 完美解决

  真的完美解决了吗?当然不,还有个小问题:给出的 p1012 ,如果直接开 long long 存下答案的话,乘法操作会溢出。解决的方法有两种:使用 __int128 类型,它可以存下最大 21271 的数;另一种是把其中一个12位乘数拆成两个6位的数,再分别进行乘法操作。见下面程序中的 mul 过程。

Code

#include<cstdio>
#include<cstring>
#include<iostream>
#define fo(i,x,y) for (int i=x;i<=y;++i)
using namespace std;

typedef long long LL;

const int maxm=2000+10, w=1e6;

LL K,n,m,p,C[2][maxm],f[maxm];

void mul(LL &x,LL y){
    LL rx=y%w*x, ry=y/w*x;
    x=(ry%p*w+rx)%p;
}
LL Pow(LL a,LL b){
    LL ret=1;
    while (b){
        if (b&1) mul(ret,a);
        mul(a,a);
        b>>=1;
    }
    return ret;
}

LL calc(LL n){
    if (!n) return 0;
    memset(C,0,sizeof(C));
    C[0][0]=1, C[0][1]=1;
    int z=0;
    fo(k,1,K){
        z=1-z; C[z][0]=1;
        f[k]=n;
        fo(i,1,k+1){
            C[z][i]=(C[1-z][i-1]+C[1-z][i])%p;
            if (i<k){
                LL t=C[z][i];
                mul(t,f[i]);
                f[k]=(f[k]+t)%p;
            }
        }
        f[k]=(Pow(n+1,k+1)-1+p-f[k])%p;
        mul(f[k],Pow(k+1,p-2));
    }
    return f[K];
}

int main(){
    freopen("box.in","r",stdin);
    freopen("box.out","w",stdout);
    scanf("%I64d%I64d%I64d%I64d",&K,&n,&m,&p);
    if (n-m<=5050){
        LL ans=0;
        while (m<=n) ans=(ans+Pow(m,K))%p, ++m;
        printf("%I64d\n",ans);
    }
    else printf("%I64d\n",(calc(n)+p-calc(m-1))%p);
    return 0;   
}

T2_互相追逐的点

  题目大意:有 n 个互相追逐的点 ( 1n20000 ),每个点 i 都有它自己的目标点 A[i](注意 A[i] 为给出的 n 个点中的一个)。已知每个点的初始坐标,开始计时后,每单位时间它们会向各自的目标点移动 1 单位距离。
  当点 i 距离它的目标点只有 1 单位距离、或者它与目标点的距离在 1 单位时间内保持不变时,它会跟随它的目标点向同一方向移动(即它的移动方向由它的目标点决定)。我们称一个点“不知道自己的移动方向”当且仅当它的移动方向由它自己决定(注意如果一个点的移动方向决定于一个“不知道自己的移动方向”的点的移动方向,那么它不能算是“不知道自己的移动方向”)。一个“不知道自己的移动方向”的点会停下不动。
  现在询问的是:在过了无限长的时间后,任意一组“不知道各自的移动方向”的点。输出任意一组且不要求输出顺序。

  看到这样的题目时我是震惊的——题目好长!(上文是题目大意,原题有1000+字数)稍微一想,如果要模拟每个点的移动,几乎是不可能的,因为一个点会向它的目标点移动,目标点又会向它的目标点移动……这种嵌套的移动模拟将会非常困难。因此我放弃了此题。
  殊不知这题其实很简单,因为你可以忽略题目中间“描述每个点的移动方式”这一段。这是因为题目要求的东西的前提是过了无限长的时间后,如果所有点在很长一段时间内仍然随着各自的目标点移动,那么它们将慢慢聚拢在一起,最终一定会全部停下。所以如果从每个点向它的目标点连一条有向边,那么就构成了一个图,而且一定有至少一个环。从任一点出发寻找环即可。时间复杂度 O(n)

Code

#include<cstdio>
#include<iostream>
#define fo(i,x,y) for (int i=x;i<=y;++i)
using namespace std;

const int maxn=200000+10;

int n,a[maxn],b[maxn],q[maxn];

int main(){
    freopen("head.in","r",stdin);
    freopen("head.out","w",stdout);
    scanf("%d",&n);
    fo(i,1,n) scanf("%d",&a[i]);
    int x=1, cnt=0;
    while (!b[x]){
        b[x]=++cnt;
        q[cnt]=x;
        x=a[x];
    }
    printf("%d\n",cnt-b[x]+1);
    fo(i,b[x],cnt) printf("%d ",q[i]);
    printf("\n");
    return 0;
}

T3_恐怖的奴隶主

  题目大意:在一个只能容纳 k 个人的场上最开始有 1 个血量为 m 的人。接下来将进行 n 个回合的操作:每个回合中设场上有 c 个人,那么场上新增最多 c 个血量为 m 的人,并保证总人数在 k 人的上限内。然后先前的 c 个人每人血量减 1,如果血量为 0 则被清除出场。问 n 个回合后场上的人数。 1n,m,k1015

  这道题的数据范围如此之大,很容易让人想到——场上人数以及血量状态有循环节!于是我们尝试模拟操作,用 A[i] 表示第 i <script type="math/tex" id="MathJax-Element-76">i</script> 回合新增的人数,观察之后能发现规律。由于规律难以解释,先贴程序,日后再作解释。

Code

#include<cstdio>
#include<iostream>
#define fo(i,x,y) for (int i=x;i<=y;++i)
using namespace std;

typedef long long LL;

LL n,m,K,a[100];

int main(){
    freopen("horrible.in","r",stdin);
    freopen("horrible.out","w",stdout);
    scanf("%I64d%I64d%I64d",&n,&m,&K);
    if (m==1){ printf("0\n"); return 0; }
    ++n;
    LL cnt=1, S=1, nxt; a[1]=1;
    while (cnt<=n){
        nxt=S;
        if (S+nxt>=K) break;
        if (cnt>=m) S-=a[cnt-m+1];
        a[++cnt]=nxt; S+=nxt;
    }
    //if (cnt==n){ printf("%I64d\n",S); return 0; }
    n=(n-cnt)%(m+1);
    if (cnt>=m){
        fo(i,1,n){
            nxt=S; S+=S;
            if (S>K) S=K;
            S-=a[(cnt++)-m+1];
        }
        printf("%I64d\n",S);
    }
    else if (!n) printf("%I64d\n",S);
    else if (n<=m-cnt) printf("%I64d\n",K);
    else printf("%I64d\n",K-a[n-m+cnt]);
    return 0;
}

总结

  这道题对我而言是极好的(因为比赛得分低的离谱),尤其是T3我到现在也不能作出详细的解释,只有个大概的了解。看来数学的基础我仍然没有掌握透彻啊……正在苦读《训练指南》补习中……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值