Test 2018-09-14

取石子游戏
$ (nim.pas/c/cpp) $

 

【题目描述】

小林和亮亮正在玩一个取石子的游戏。
石子一共有 $ n $ 堆,其中第 $ i $ 堆恰好有 $ i $ 粒石子。小林先取,亮亮后取,并且两人依次轮流取石。
每一次取石子的人可以选择任意一堆还未被取完的石子,并取走这一堆中任意多粒石子
(注意,不能一粒石子也不取,也不能同时在多堆石 子中取石)。最终,无石可取的人为败。
 
小林和亮亮都十分聪明,他们的每次取石都会采取最优策略。
在经过多次游戏后,小林发现了先手必胜的条件,但他不满足于此,
他想知道,在知道石子的堆数 $ n $ 后,他第一次取石有多少种方式可以获胜。
 

【输入格式】

第一行一个整数 $ T $ ,表示数据组数。
接下来 $ T $ 行每行一个整数 $ n $ ,表示石子的堆数。
 

【输出格式】

每组数据输出一个整数,表示在两个人都采用最佳策略的情况下,
小林第一次取石能够获胜的方式数,如小林必败,则输出 $ 0 $ 。
 

【样例输入】
 2
 2
 3
【样例输出】
 1
 0 

 

【样例解释】

$ n=2 $ 时小林只有一种取胜方式,即取在第二堆石子中取一粒。
$ n=3 $ 时小林必败,因此输出 $ 0 $ 。
 

【数据规模】

对于 $ 20 $ %的数据,$ n \le 10 $ ;
对于 $ 50 $ %的数据,$ n \le 1000 $ ;
对于 $ 90 $ %的数据,$ n \le 10^15 $ ;
对于 $ 100 $ %的数据,$ 1 \le n \le 10^1000,1 \le T \le 10 $ 。
 

【题解】

算法一:动态规划或记忆化搜索
期望得分:$ 20 $
 
算法二:此题实际上为 $ Nim $ 游戏,根据 $ Nim $ 游戏的结论,先手必胜当且仅当每一堆石子的石子数的异或和不为 $ 0 $ 。
枚举第一次在哪一堆取几粒石子,看剩下的数异或和是否为 $ 0 $ 。由于异或的逆运算仍然是异或,
因此可以先求出所有数的异或和再每次调整,减少计算量。
时间复杂度:$ O(n^2) $
期望得分:$ 50 $
 
算法三:注意到 $ x $ 为偶数时 $ x \quad xor \quad (x+1)= 1 $
(可以在二进制下看这个结果,其中^为异或运算符), 可以发现 $ 1~n $ 的异或和 $ S $ 有以下规律:

$ 1. S=1 \quad \quad n $ % $ 4 =1 $
$ 2. S=n+1 \quad n $ % $ 4=2 $
$ 3. S=0 \quad \quad n $ % $ 4 = 3 $
$ 4. S=n \quad \quad n $ % $ 4 = 0 $

$ n $ % $ 4=3 $ 时,先手必败,答案为 $ 0 $ 。
$ n $ % $ 4=1 $ 时,在任意编号为奇数的石子堆中取 $ 1 $ 可使异或和变为 $ 0 $ ,且只有这些方案,于是答案为 $ (n+1)/2 $ 。
$ n $ % $ 4=0 $ 或 $ 2 $ 时,$ S $ 与 $ n $ 位数相同。设它们的 最高位为 $ t,S $ 的第 $ t $ 位为 $ 1 $ ,
为使 $ S $ 变为 $ 0 $ ,必须修改一个第 $ t $ 位为 $ 1 $ 的数。
显然修改任一第 $ t $ 位为 $ 1 $ 的数都可以使 $ S $ 变为 $ 0 $ 。答案即为 $ n $ 去掉最高位再加 $ 1 $ 。
时间复杂度:$ O(1) $
期望得分:$ 90 $
 
算法四:
在算法三基础上加上高精度即可得到 $ 100 $ 分。
 

【代码】

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

struct nim
{
    int len, value[1010];
    nim() { len = 1; memset(value, 0, sizeof(value)); }
    
    nim operator =(const char* s)
    {
        len = strlen(s);
        memset(value, 0, sizeof(value));
        for (int i = 0; i < len; i++)
        {
            value[i] = s[len - i - 1] - 48;
        }
        return *this;
    }
    
    nim operator =(const int& x)
    {
        char s[1010];
        sprintf(s, "%d", x);
        *this = s;
        return *this;
    }
    
    nim operator +(const nim& x) const
    {
        nim y;
        y.len = max(len, x.len);
        for (int i = 0; i < len; i++)
            y.value[i] = x.value[i] + value[i];
        int k = 0;
        for (int i = 0; i < y.len; i++)
        {
            k += y.value[i];
            y.value[i] = k % 10;
            k /= 10;
        }
        if (k > 0)
        {
            y.value[y.len] = k;
            y.len++;
        }
        return y;
    }
    
    nim operator += (const nim& x)
    {
        *this = *this + x;
        return *this;
    }
    
    nim operator /= (const int& x) 
    {
        int p = 0;
        for (int i = len - 1; i >= 0; i--)
        {
            p = p * 10 + value[i];
            value[i] = p / x;
            p %= x;
        }
        if (!value[len-1]) len--;
        return *this;
    }
    
    nim operator *= (const int& x)
    {
        int p = 0;
        for (int i = 0; i < len; i++)
        {
            p += value[i] * x;
            value[i] = p % 10;
            p /= 10;
        }
        if (p > 0) { value[len] = p; len++; }
        return *this;
    }
    
    bool operator <=(const nim& x) const
    {
        if (len != x.len) return len < x.len;
        for (int i = len - 1; i >= 0; i--)
        {
            if (value[i] < x.value[i]) return true;
            if (value[i] > x.value[i]) return false;
        }
        return true;
    }
    
    nim operator -=(const nim& x) 
    {
        for (int i = 0; i < len; i++)
            value[i] -= x.value[i];
        for (int i = 0; i < len; i++)
            if (value[i] < 0) { value[i] += 10; value[i+1]--; }
        while (len > 1 && value[len-1] == 0) len--;
        return *this;
    }
}a, b;

void writeint(nim x)
{
    for (int i = x.len - 1; i >= 0; i--)
        printf("%d", x.value[i]);
    printf("\n");
}

int main()
{
    freopen("nim.in", "r", stdin);
    freopen("nim.out", "w", stdout);
    int T;
    scanf("%d", &T);
    while (T--)
    {
        char s[1010];
        scanf("%s", s);
        int n = strlen(s);
        if ((s[n-1] - 48) % 2)
        {
            a = s, b = 1, a += b;
            int k = s[n-1] - 48 + 1;
            if (n > 1) k += (s[n-2] - 48) * 10;
            if (! (k % 4) ) { printf("0\n"); continue; }
            a /= 2;
            writeint(a);
            continue;
        }
        else
        {
            b = 1;
            a = s;
            while (b <= a) b *= 2;
            b /= 2;
            a -= b;
            b = 1;
            a += b;
            writeint(a);
        }
    }
    return 0;
}

魔数
$ (number.pas/c/cpp) $

 

【题目描述】

小林和亮亮是好朋友。小林有一个幸运数字 $ a $ ,亮亮有一个幸运数字 $ b $ 。
一 个数字称之为“幸运数”当且仅当它只由 $ a $ 和 $ b $ 组成。
小林称一个数为“魔数”, 当且仅当这个数各数位之和为“幸运数”,且这个数字本身也是“幸运数”。
 
举个例子:小林的幸运数字为 $ 2 $ ,亮亮的幸运数字为 $ 6 $ ,那么 $ 23 $ 不是“幸运数”,而 26、222 是“幸运数”。
进一步,$ 222 $ 是“魔数”(因为 $ 2+2+2=6 $ ),而 $ 26 $ 不是“魔数”(因为 $ 2+6=8 $ )。
 
亮亮想要知道,有多少个 $ n $ 位的“魔数”(一个 $ n $ 位数不包含前导 $ 0 $ ),
由于这个数字会很大,所以亮亮只关心这个数模 $ 1000000007(10^9+7 $ ,是一个质数 $ )$ 的结果。
 

【输入格式】

只有一行,包含三个整数:$ a、b、n $ 。意义见题目描述。
 

【输出格式】

只有一个整数,表示 $ n $ 位的“魔数”的个数模 $ 1000000007 $ 的结果。
 

【样例输入】
 2 6 3
【样例输出】
 1

 

【样例解释】

两个幸运数字分别为 $ 2 $ 和 $ 6 $ ,则位数为 $ 3 $ 的“魔数”只有 $ 222 $ 一个。
 

【数据规模】

对于 $ 30 $ %的数据:$ 1≤n≤5 $ ;
对于 $ 60 $ %的数据:$ 1≤n≤100 $ ;
对于 $ 100 $ %的数据:$ 1≤a<b≤9,1≤n≤10^6 $ 。
 

【题解】

算法一:枚举每一个 $ n $ 位数,判断它是否是“魔数”。
时间复杂度:$ O(10^n) $
期望得分:$ 30 $
 
算法二:由于“魔数”首先是“幸运数”,所以它只能由 $ a $ 和 $ b $ 组成,
那么我们枚举 $ a $ 的个数 $ i(0 \le i \le n)$ , 并判断由 $ i $ 个 $ a $ 和 $ (n-i) $ 个 $ b $ 组成的数字是不是“魔数”,
也就是判断 $ a \times i+(n-i) \times b $ 是否为“幸运数”,如果是,那么我们就找到了一类魔数,由排列组合知识可知
这一类魔数共有 $ C^i_n = \frac{n!}{i!(n-i)!} $ 个。所以我们只需要求一系列的组合数之和即可。由
于模数 $ 10^9+7 $ 是一个质数,所以我们在作除法时可以用求逆元的方法进行。
时间复杂度:$ O(n^2) $
期望得分:$ 60 $
 
算法三:算法二的问题在于,每一次求组合数都需要 $ O(n) $ 的时间,然而我们发现,
我们要求的组合数都是 $ C^k_n $ 的形式($ n $ 是固定的),而在求出 $ C^k_n $ 后,
求 $ C^{k+1}_n $ 只需利用公式 $ C^{k+1}_n = C^k_n \times \frac{n-k}{k+1} $ ,只有 $ O(1) $ 的复杂度,这就解决了算法二超时的问题。
另外,我们可以采用求逆元的方法来解决除法的问题,也可以把每一个因子和除数都分解质因数,
这样数的乘除运算就转化成了各个质数指数的加减运算。
用标准分解式来保存大数,就避免了用扩展欧几里得算法求逆元。
时间复杂度:$ O(n) $
期望得分:$ 100 $
 

【代码】

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define int long long
#define Mod 1000000007
int a,b,Mx,n,ans,f[1000005];
bool pd(int x){
    while(x){
        if(x%10!=a&&x%10!=b) return 0;
        x/=10;
    }
    return 1;
}
int qpow(int x){
    int res=x,k=Mod-3;
    while(k){
        if(k&1) res=res*x%Mod;
        x=x*x%Mod;
        k>>=1;
    }
    return res;
}
signed main(){
    freopen("number.in","r",stdin);
    freopen("number.out","w",stdout);
    scanf("%lld %lld %lld",&a,&b,&n);
    f[0]=1;
    for(int i=1;i<=n;++i) f[i]=(f[i-1]*i)%Mod;
    int res=n*b; ans+=pd(res);
    for(int i=1;i<=n;++i){
        res=res-b+a;
        if(pd(res)) ans=(ans+f[n]*qpow(f[i])%Mod*qpow(f[n-i])%Mod)%Mod;
    }
    printf("%lld",ans);
    return 0;
}

军训站队
$ (queue.pas/c/cpp) $

 

【题目描述】

小林和亮亮刚入高中,首先就迎来了军训。这一天,他们班的同学正在教官 的指导下进行队列练习。
 
班上总共有 $ n $ 位同学,他们站成一排,然而,本该站成一条直线的他们却排 成了“一字长蛇阵”。
用数学的语言来描述,如果把训练场看成一个平面直角坐标系,第 $ i $ 名同学所在位置的横坐标是 $ i $ ,
而所有同学的纵坐标本该是 $ 0 $(或者任 意一个相等的常量),这样就排成了一条直线。
当然,由于同学们排的歪歪扭扭, 所以第 $ i $ 位同学的横坐标依然是 $ i $ ,而纵坐标却成了 $ Y_i $
(为了简化问题,我们假设所有的坐标都是整数)。
 
对此,教官当然十分生气,因此他决定下命令调整队伍,使得所有人能够站成一条直线(也即让所有的 $ Y_i $ 相同)。
教官的命令总共有三种:
 
1、除了某一个同学外,其余所有同学向前走一步(向前走一步可理解为 $ Y_i $ 的 值加 $ 1 $ ,下同);
2、除了某一个同学外,其余所有同学向前走两步;
3、除了某一个同学外,其余所有同学向前走五步。
 
教官希望他能以最少的命令次数使得所有同学站成一条直线,但是他不会算, 于是就让亮亮帮他来计算。
亮亮虽然聪明,可是由于班上同学人数众多,他一下子也解决不了这个问题,只能来寻求会编程的你的帮助,你能告诉亮亮答案吗?
 

【输入格式】

第一行有一个整数 $ n $ ,表示班上共有 $ n $ 位同学。
第二行有 $ n $ 个整数,第 $ i $ 个整数 $ Y_i $ 表示第 $ i $ 位同学初始位置的纵坐标。
 

【输出格式】

一个整数,表示最少的下达命令次数。
 

【样例输入】
 4
 1 1 2 6
【样例输出】
 2

 

【样例解释】

一种方案是:$ 1126 → 2227 → 7777 $
 

【数据规模】

对于 $ 40 $ %的数据,$ n \le 10, Y_i \le 10 $ ;
对于 $ 100 $ %的数据,$ 1 \le n \le 100000,0 \le Y_i \le 1,000,000,000 $ 。
 

【题解】

算法一:当数据规模很小时,可以采用记忆化搜索或者暴力动态规划的方法。
期望得分:$ 40 $
 
算法二:我们发现,排成一条直线是一个相对的概念(它只与相对位置有关,而与绝对位置 无关),
因此如果一个人不动,其余同学都向前走 $ A $ 步,相当于其他同学都不动,而此人向 后走 $ A $ 步。
那么三种指令对应于指定 $ i $,令 $ Y_i $ 的值减 $ 1 $ ,减 $ 2 $ ,或减 $ 5 $ 。
如果我们将所有 $ Y_i $ 从小到大进行排序,那么最终所有 $ Y_i $ 都应该减少至 $ Y_1 -1, Y_1-2,...,Y_1 - 4 $ 这五个值之一。
那么我们枚举所有的 $ Y_i $ 该减少到哪一个值,计算出每一个 $ Y_i $ 减少到该值所需的指令数
(这很容易计算,先减五,减不动了减二,再减不动了减一 即可)并求和,最终比较减少到哪一个值所用指令数最少,即为所求答案。
时间复杂度:$ O(n \times log_n) $ 或 $ O(n) $
期望得分:$ 100 $
 

【代码】

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

int n, a[100001], b[100001];
long long ans = 0, ss;

int main()
{
    freopen("queue.in", "r", stdin);
    freopen("queue.out", "w", stdout);
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    sort(a + 1, a + n + 1);
    for (int i = n; i >= 1; i--)
        a[i] -= a[1];
    memcpy(b, a, sizeof(b));
    for (int j = 0; j < 5; j++)
    {
        ans = 0;
        memcpy(a, b, sizeof(b));
        for (int i = 1; i <= n; i++)
            a[i] += j;
        for (int i = 1; i <= n; i++)
        {
            if (a[i] >= 5) ans += (long long)a[i]/5, a[i] %= 5;
            if (a[i] >= 2) ans += (long long)a[i]/2, a[i] %= 2;
            if (a[i] > 0) ans += (long long)a[i], a[i] = 0;
        }
        if (j == 0) ss = ans; else ss = min(ss, ans);
    }
    printf("%lld\n", ss);
    return 0;
}

转载于:https://www.cnblogs.com/PotremZ/p/Test20180914.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值