取石子游戏
【题目描述】
小林和亮亮正在玩一个取石子的游戏。
石子一共有 $ 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("
");
}
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
"); 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;
}
魔数
【题目描述】
小林和亮亮是好朋友。小林有一个幸运数字 $ 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 imes i+(n-i) imes 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 imes 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;
}
军训站队
【题目描述】
小林和亮亮刚入高中,首先就迎来了军训。这一天,他们班的同学正在教官 的指导下进行队列练习。
班上总共有 $ 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 imes 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
", ss);
return 0;
}