一.年龄问题
题目描述
本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。
S 夫人一向很神秘。这会儿有人问起她的年龄,她想了想说: "20 年前,我丈夫的年龄刚好是我的 2 倍,而现在他的年龄刚好是我的 1.5 倍"。
你能算出 S 夫人现在的年龄吗?
运行限制
- 最大运行时间:1s
- 最大运行内存: 128M
思路:1.数学解法,设我20年前为x,则丈夫为2*x,20年后,我:x+20,丈夫:2*x+20,则有1.5*(x+20)=2*x+20,可知x=40。
2.计算机枚举,年龄一定是有限整数。
#include <iostream>
using namespace std;
int main()
{
int wife, husband;
for(wife = 1;; ++ wife)
{
husband = wife << 1;
//cout << wife << " " << husband << endl;
if((husband + 20) * 2 == (wife + 20) * 3)
{
cout << wife + 20;
break;
}
}
return 0;
}
二.海盗与金币
题目描述
本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。
12 名海盗在一个小岛上发现了大量的金币,后统计一共有将近 5 万枚。
登上小岛是在夜里,天气又不好。由于各种原因,有的海盗偷拿了很多,有的拿了很少。
后来为了“均贫富”,头目提出一个很奇怪的方案:
每名海盗都把自己拿到的金币放在桌上。然后开始一个游戏。
金币最多的海盗要拿出自己的金币来补偿其他人。
补偿的额度为正好使被补偿人的金币数目翻番(即变为原来的 2 倍)。
游戏要一直进行下去,直到无法完成。
(当金币数最多的不只一个人或最多金币的人持有金币数不够补偿他人的)
游戏就这样紧张地进行了,一直进行了 12 轮,恰好每人都“放血”一次, 更离奇的是,刚好在第 12 轮后,每个人的金币数居然都相等了!! 这难道是天意吗?
请你计算,游戏开始前,所有海盗的初始金币数目,从小到大排列,中间有一个空格分开。
答案形如:
8 15 29 58 110 ...
当然,这个不是正确答案。
运行限制
- 最大运行时间:1s
- 最大运行内存: 128M
坑点:本题需要注意,金币最多的海盗要拿出自己的金币来补偿其他人,这里是补偿其他所有人,不是补偿钱数最少的人。
思路:1.正向枚举,由题意可知总数<=50000,且一定是12的倍数(否则是怎么最终分成12等分的)。
2.反向递推,枚举最终每一个人手上的硬币数量(或者金币总数),假设为x,假设第12轮,A是补偿者,其他人是被补偿者,那么可知
还注意到题目中所说的每个人都放血了一次,因此本题直接往回推即可。
#include <iostream>
using namespace std;
bool DFS(int i, int person[])
{
if(i == 12)
{
return true;
}
for(int j = 0; j < 12; ++ j)
{
if(i == j) continue;
if(person[j] & 1)
{
return false;
}
else
{
person[j] >>= 1;
person[i] += person[j];
}
}
return DFS(i + 1, person);
}
int main()
{
int money, coin, i;
int person[12];
for(money = 12; money <= 50000; money += 12)
{
coin = money / 12;
for(i = 0; i < 12; ++ i)
{
person[i] = coin;
}
if(DFS(0, person))
{
for(i = 0; i < 12; ++ i)
{
cout << person[i] << " ";
}
break;
}
}
return 0;
}
三.全排列
题目描述
本题为代码补全填空题,请将题目中给出的源代码补全,并复制到右侧代码框中,选择对应的编译语言(C/Java)后进行提交。若题目中给出的源代码语言不唯一,则只需选择其一进行补全提交即可。复制后需将源代码中填空部分的下划线删掉,填上你的答案。提交后若未能通过,除考虑填空部分出错外,还需注意是否因在复制后有改动非填空部分产生错误。
对于某个串,比如:“1234”,求它的所有全排列。 并且要求这些全排列一定要按照字母的升序排列。
对于“1234”,应该输出(一共4!=24行):
1234
1243
1324
1342
1423
1432
2134
2143
2314
2341
2413
2431
3124
3142
3214
3241
3412
3421
4123
4132
4213
4231
4312
4321
下面是实现程序,请仔细分析程序逻辑,并填写划线部分缺少的代码。
源代码
C
#include <stdio.h>
#include <string.h>
//轮换前n个,再递归处理
void permu(char* data, int cur)
{
int i,j;
if(data[cur]=='\0'){
printf("%s\n", data);
return;
}
for(i=cur; data[i]; i++){
char tmp = data[i];
for(j=i-1; j>=cur; j--) data[j+1] = data[j];
data[cur] = tmp;
permu(data, cur+1);
tmp = data[cur];
___________________________________ ; //填空
data[i] = tmp;
}
}
int main()
{
char a[105];
scanf("%s",a);
permu(a,0);
return 0;
}
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
思路:这题是典型的DFS生成所有的可能状态的问题,缺少的部分是回溯时进行状态的恢复。
#include <stdio.h>
#include <string.h>
//轮换前n个,再递归处理
void permu(char* data, int cur)
{
int i,j;
if(data[cur]=='\0'){
printf("%s\n", data);
return;
}
for(i=cur; data[i]; i++){
char tmp = data[i];
for(j=i-1; j>=cur; j--) data[j+1] = data[j];
data[cur] = tmp;
permu(data, cur+1);
tmp = data[cur];
for(j = cur; j < i; ++ j) data[j] = data[j + 1];
data[i] = tmp;
}
}
int main()
{
char a[105];
scanf("%s",a);
permu(a,0);
return 0;
}
四.约瑟夫环
题目描述
n 个人的编号是 1 ~ n,如果他们依编号按顺时针排成一个圆圈,从编号是 1 的人开始顺时针报数。
(报数是从 1 报起)当报到 k 的时候,这个人就退出游戏圈。下一个人重新从 1 开始报数。
求最后剩下的人的编号。这就是著名的约瑟夫环问题。
本题目就是已知 n,k 的情况下,求最后剩下的人的编号。
输入描述
输入是一行,2 个空格分开的整数 n,k (0<n,k<10^7)。
输出描述
要求输出一个整数,表示最后剩下的人的编号。
输入输出样例
示例
输入
10 3
输出
4
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
思路:1.用循环链表或者队列进行模拟。
2.动态规划思想:我们先来看看在n、k的规模下,如果剔除掉k这个人,会发生什么(简单起见我们假设n>k)。
n=11,k=3的情况(黑色为删除k之前,红色为删除k之后)
很明显,当删除k之后,约瑟夫环开始重新数数,此时规模为n-1,k。删除前索引为m的位置,删除后索引为((m-k)+n)%n。
我们用f(n,k)来描述状态,那么删除一个元素后,状态变为f(n-1,k),但是需要注意,根据上图,删除元素后要重新数数,因为元素的索引会发生变化,即有
现在我们用dp[i]表示规模为i、数数为k时,能够剩下的那个幸运儿。在i=1的时候,不会有元素剩下,dp[1]=0。其他的可用递推公式dp[i]=(dp[i-1]+k)%i。
需要注意的是,由于取模的存在,1~n会变为0~n-1,此时答案需要+1。
#include <iostream>
using namespace std;
int main()
{
int dp = 0, n, k;
cin >> n >> k;
for(int i = 2; i <= n; ++ i)
{
dp = (dp + k) % i;
}
cout << dp + 1;
return 0;
}
五.交换次数
题目描述
IT 产业人才需求节节攀升。业内巨头百度、阿里巴巴、腾讯(简称 BAT )在某海滩进行招聘活动。
招聘部门一字排开。由于是自由抢占席位,三大公司的席位随机交错在一起,形如:BABTATT,这使得应聘者十分别扭。
于是,管理部门要求招聘方进行必要的交换位置,使得每个集团的席位都挨在一起。即最后形如:BBAAATTT 这样的形状,当然,也可能是:AAABBTTT 等。
现在,假设每次只能交换 2 个席位,并且知道现在的席位分布,你的任务是计算:要使每个集团的招聘席位都挨在一起需要至少进行多少次交换动作。
输入描述
输入是一行 n 个字符(只含有字母 B、A 或 T ),表示现在的席位分布。
输出描述
输出是一个整数,表示至少交换次数。
输入输出样例
示例
输入
TABTABBTTTT
输出
3
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
思路:首先我们可以明确,最终的状态只有六种A...B...T...,A...T...B...,B...A...T,B...T...A,T...A...B,T...B...A...。此时我们只需要分别计算初始状态到达这六种合法状态的六种方案的最小代价就可以了。
解法1:直接BFS。
解法2:由于本题说的是任意交换而不是相邻交换(如果是相邻交换的话,最小步骤就是逆序对的个数),因此我们需要探讨一下计算公式。
以TABTABBTTTT为例,假设我们最后要达到的状态为AABBBTTTTTT:
我们可以如上图描述的那样,将原串当作A、B、T的三个区间,那我们要达到最终的状态,步骤为将A区间中的非A字符交换出去,将B区间中的非B字符交换出去,当A、B区间都变为全A和全B的状态时,T区间必是全T的状态。
注意,当A区间中的B与B区间中的A交换时,只需要交换一次就可以,但是上述计算方式我们会算作两次,这就需要用到容斥原理进行去重。重复的量为。因此,最终的答案为
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int getAns(string perm, int n_one, int n_two, char c_one, char c_two)
{
int oneOfTwo = 0, twoOfOne = 0, notOne = 0, notTwo = 0, i;
for(i = 0; i < n_one; ++ i)
{
if(perm[i] != c_one)
{
++ notOne;
if(perm[i] == c_two)
{
++ twoOfOne;
}
}
}
for(; i < n_two + n_one ; ++ i)
{
if(perm[i] != c_two)
{
++ notTwo;
if(perm[i] == c_one)
{
++ oneOfTwo;
}
}
}
//cout << notOne << " " << twoOfOne << " " << notTwo << " " << oneOfTwo << endl;
return notOne + notTwo - min(twoOfOne, oneOfTwo);
}
int main()
{
string perm;
cin >> perm;
int c_A = 0, c_B = 0, c_T = 0, n = perm.size(), i, ans = 0x3f3f3f3f;
for(i = 0; i < n; ++ i)
{
switch(perm[i])
{
case 'A':++ c_A;break;
case 'B':++ c_B;break;
case 'T':++ c_T;break;
}
}
//cout << c_A << " " << c_B << " " << c_T << " " << n << endl;
ans = min(ans, getAns(perm, c_A, c_B, 'A', 'B'));
//cout << ans << endl;
ans = min(ans, getAns(perm, c_A, c_T, 'A', 'T'));
//cout << ans;
ans = min(ans, getAns(perm, c_B, c_A, 'B', 'A'));
//cout << ans;
ans = min(ans, getAns(perm, c_B, c_T, 'B', 'T'));
//cout << ans;
ans = min(ans, getAns(perm, c_T, c_A, 'T', 'A'));
//cout << ans;
ans = min(ans, getAns(perm, c_T, c_B, 'T', 'B'));
cout << ans;
return 0;
}
六.自描述序列(太难太乱了吧喂)
题目描述
小明在研究一个序列,叫 Golomb 自描述序列,不妨将其记作 G(n)。这个序列有 2 个很有趣的性质:
-
对于任意正整数 n,n 在整个序列中恰好出现 G(n) 次。
-
这个序列是不下降的。
以下是 G(n) 的前几项:
n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
G(n) | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 6 | 6 |
给定一个整数 n,你能帮小明算出 G(n) 的值吗?
输入描述
输入一个整数 n (1≤n≤20^15)。
输出描述
输出一个整数 G(n)。
输入输出样例
示例
输入
13
输出
6
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
思路:首先理清题意
在初始化的时候,G(1)=1代表G序列中有1个1,G(2)=2代表G序列中有两个2,因此状态如下:
n | 1 | 2 | |||||||||||
G(n) | 1 | 2 | 2 |
接下来,我们看到n=3,发现G(3)=2,因此接下来在G序列中填两个3
n | 1 | 2 | 3 | ||||||||||
G(n) | 1 | 2 | 2 | 3 | 3 |
后面就依此类推(阅读理解杯名不虚传。。。。。。)
n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
G(n) | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 6 | 6 | 6 | 6 | 7 | 7 | 7 | 7 |
1.直接开数组模拟。
#include <iostream>
using namespace std;
const int N = 5e7;
int G[N];
int main()
{
int n;
cin >> n;
G[1] = 1;
G[2] = G[3] = 2;
if (n <= 3)
{
cout << G[n];
}
else
{
int cnt = 3, temp;
for (int i = 3; i <= n; ++i)
{
temp = G[i];
while (temp--)
{
G[++cnt] = i;
if (cnt == n)
{
cout << G[n];
return 0;
}
}
}
}
return 0;
}
2.直接打表一批数据,一般来说打表题会有显而易见的规律(这题不是很显而易见)。
3.G(n)的增长比n要慢得多,多个连续的n值会映射到同一个G(n)值,因此我们可以找到G(n)值发生改变的地方,找规律。
n | 1 | 3 | 5 | 8 | 11 | 15 | 19 |
G(n) | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
我们定义G(n)的反函数(实际上并不是严格的反函数),f(G(n))=n,n为在G(n)取相同值时,最大的那个n,例如G(2)=G(3)=2,则f(2)=3;则有
G(n) | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
f(G(n))=n | 1 | 3 | 5 | 8 | 11 | 15 | 19 |
我们发现f(G(n))的增长速度非常快,因此我们枚举G(n),只要f(G(n))>=tar,则f(G(n))就是答案。
接下来我们来探讨一下f(G(n))的变化规律:f(x)=f(x-1)+delta。
delta是一个会在某个条件上触发自增的量,我们来探究一下条件:
1.我们已经定义了一个“反函数”:f(x)表示当G(n)=x时最大的那个n。那么如果说我们有一个n'=n+1,自然就知道G(n')=G(n)+1。
2.由G(n)序列的定义,我们可以知道,G(n)序列的长度为G(1)+G(2)+...+G(n)+...。
3.细想一下(似懂非懂)会发现当G(n)+1的时候,正好对应delta+1(其实这是因为反函数带来的G(n)所具有的性质)。
4.对于f(x),当x>f(delta)的时候,delta需要自增。可以这么理解,f(delta)表示现在已经考虑的G(n)序列的范围的右边界,当x>f(delta)即超出边界时,自然要通过扩大边界使得x在区间内。
5.初始化的时候,delta=1。
#include <iostream>
using namespace std;
const int N = 3e7;
long long f[N];
int main()
{
int n, delta, i;
cin >> n;
f[1] = delta = 1;
for(i = 2; ; ++ i)
{
if(i > f[delta])
{
++ delta;
}
f[i] = f[i - 1] + delta;
if(f[i] >= n)
{
cout << i;
break;
}
}
return 0;
}
改进:f(G(n))已经增长很快了,但还不够。我们上面已经找到了delta跳变的规律,为了找到增长更快的函数,我们以l是否跳变为界限,可以得到下面一个更加快速的函数:
delta | 1 | 2 | 3 |
f(delta)=n | 1 | 5 | 11 |
根据我们上面对于f的定义,f(G(n))表示在G(n)取相同值时的右边界,那么易知f(i)-f(i-1)则表示G(n)=i出现的次数,于是我们可以计算出delta跳变的位置。设h[i]表示delta=i时的最大位置(delta即将要发生跳变的位置),那么可知h[i]=h[i-1]+i*(f[i]-f[i-1])。
当我们有h[i]>=n的时候,可以知道n一定满足n∈(h[i-1],h[i]]。我们现在知道的是当n处于的现在的区间时的delta为多少,现在就是根据这个delta反推G(n)。
条件1:f[delta]表示G(n)=delta时的右边界
条件2:n处于的区间有x个G(n')=delta
以表格为例,我们来看一下具体计算过程:
n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
G(n) | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 6 | 6 | 6 | 6 | 7 | 7 | 7 | 7 | 8 | 8 | 8 | 8 |
f(G(n))表示在取相同的G(n)值的情况下的最大的n值。
G(n) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
f(G(n))=n | 1 | 3 | 5 | 8 | 11 | 15 | 19 | 23 |
h(i)表示在f(G(n))的增量delta=i时的n值。
i | 1 | 2 | 3 | 4 |
h(i) | 1 | 5 | 11 | 23 |
假设我们要知道G(13),此时查询表3可知,13<h(4)=23,且(23-13)/4=2,距离h(i)=23有两个完整的4,也就是说当前还差两个增量4就到n。
由f(G(n))的定义,我们关注G(n)=3,f(3)=5,与G(n)=4,f(4)=8这两对值,可以知道当n∈(5,8)时,G(n)=4,并且在这个区间内,每出现一次G(n),相当于一个增量4。
现在距离n有两个增量,而f(4)=8,则G(13)=f(4)-(h[4]-n)/4=8-(23-13)/4=8-2=6。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2e6;
long long f[N], h[N];
int main()
{
long long delta, i, n;
cin >> n;
h[1] = f[1] = delta = 1;
for (i = 2; i < N; ++i)
{
if (i > f[delta])
{
++delta;
}
f[i] = f[i - 1] + delta;
}
for (i = 2; ; ++i)
{
h[i] = h[i - 1] + (f[i] - f[i - 1]) * i;
if (h[i] > n)
{
cout << f[i] - (h[i] - n) / i;
break;
}
}
return 0;
}