昨天上午是头条2018暑期实习在线笔试,时限2小时,5道题,OI赛制,有部分分。难度比较大。
对于这种时间紧,题目多而且难度大的笔试,建议如果题目没思路,就先写暴力算法。
题目1
对于严格递增的正整数数列 A=a1,a2,…an,如果一个正整数 T 满足:
- 对于数列 A 中的任意元素 x,如果 x+T 不大于 an,则 x+T 也是 A 中的元素;
- 对于数列 A 中的任意元素 x,如果 x−T 不小于 a1,则 x−T 也是 A 中的元素;
那么称 T 为数列 A 的周期,如果同时满足:所有小于 T 的正整数,都不是 A 的周期,则称 T 为 A 的最小周期。
输入描述:
每组测试样本的输入格式为:
第一行是一个正整数 N;
从第二行开始,每行有若干个正整数,依次存放 n,a1,a2,…an,一共有 N 行,也就是 N 个数列。
输出描述:
输出有 N 行,每行打印出对应数列的最小周期。
样例:
输入:
3
3 1 2 3
3 2 4 6
3 3 4 6
输出:
1
2
3
说明:
数据范围:
N:0<N<10
an:an<109
n:
0<n<103(50%)
0<n<104(30%)
0<n<106(20%)
算法
(KMP) O(n)
题目对数列定义了一种新的周期 T,难以求直接解。我们先将问题转化为求数列的循环节长度 t。
首先将数列:a0,a1,…an−1 转换成 b1,b2,…bn−1,其中bi=ai−ai−1。
则 t 是 {bi} 的循环节,当且仅当 T=at−a0 是数列 {ai} 的周期;
由于数列 {ai} 严格单调递增,所以t 是 {bi} 的最短循环节,当且仅当 T=at−a0 是数列 {ai} 的最小周期。
求数列的最短循环节是一个经典问题,可以用KMP算法来做。
首先求出数列{bi}的 next 数组,nexti 是满足在 {bi} 中 [1,nexti] 和 [i−nexti+1,i] 这两段连续子序列相等的最大值。
则 t=(n−1)−nextn−1。
简单解释一下为什么 t 是最短循环节:
根据 next 数组的定义,在数列 {bi} 中,连续子序列 [(n−1)−nextn−1+1,n−1] 和连续子序列 [1,nextn−1] 相等,所以我们会依次得到:
- 连续子序列 [(n−1)−t+1,n−1] 等于连续子序列 [(n−1)−2t+1,(n−1)−t];
- 连续子序列 [(n−1)−2t+1,(n−1)−t] 等于连续子序列 [(n−1)−3t+1,(n−1)−2t];
- 依次类推,可得 t 是数列 {bi} 的循环节。且由于 nexti 是满足要求的最大值,所以 t 是最短循环节。
感觉太抽象的同学可以自行画张线段图。 ^^
求出 t 后, T=at−a0。
时间复杂度分析:数列变换和KMP算法的时间复杂度均是 O(n),所以总时间复杂度 O(n)。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1000010;
int n;
int a[N], b[N];
int nxt[N];
void get_next()
{
for (int i = 2, j = 0; i < n; i++)
{
while (j && b[i] != b[j + 1]) j = nxt[j];
if (b[i] == b[j + 1]) j++;
nxt[i] = j;
}
}
int main()
{
int cases;
cin >> cases;
while (cases--)
{
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 1; i < n; i++) b[i] = a[i] - a[i - 1];
get_next();
int t = n - 1 - nxt[n - 1];
cout << a[t] - a[0] << endl;
}
return 0;
}
题目2
现在有 n1+n2 种面值的硬币,其中前 n1 种为普通币,可以取任意枚,后 n2 种为纪念币,每种最多只能取1枚,每种硬币有一个面值,问能用多少种方法拼出 m 的面值?
输入描述:
第一行三个整数 n1,n2,m,分别表示普通币种类数,纪念币种类数和目标面值;
第二行 n1 个整数,第 i 种普通币的面值 a[i]。保证 a[i] 为严格升序;
第三行 n2 个整数,第 i 中纪念币的面试 b[i]。保证 b[i] 为严格升序;
对于 30% 的测试,保证 1≤n1+n2≤10, 1≤m≤100, 1≤a[i]≤100, 1≤b[i]≤100
对于 100% 的测试,保证 1≤n1+n2≤100, 1≤m≤100000, 1≤a[i]≤100000, 1≤b[i]≤100000
输出描述:
输出第一行,包含一个数字 x,表示方法总数对 1000000007(1e9+7) 取模的结果。
注意:不要忘记取模!
样例:
输入:
3 1 5
1 2 3
1
输出:
9
说明:
(x) 代表面值为x的普通币,[x]代表面值为x的纪念币,样例所有方法数如下:
(1)(1)(1)(1)(1)
(1)(1)(1)(2)
(1)(1)(3)
(1)(2)(2)
(2)(3)
(1)(1)(1)(1)[1]
(1)(1)[1](2)
(1)[1](3)
[1](2)(2)
算法
(背包问题) O(mn)
典型的背包问题,普通币是完全背包问题,纪念币是0/1背包问题。
两种背包问题做法类似,都是先枚举物品,再枚举容量,不同点在于完全背包问题要从小到大枚举容量,0/1背包问题要从大到小枚举容量。
时间复杂度分析:两重循环,复杂度 O(mn)=100∗100000=107,1秒内出解毫无压力。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, M = 100010, MOD = 1000000007;
int n1, n2, m;
int a[N], b[N], f[M];
int main()
{
cin >> n1 >> n2 >> m;
for (int i = 0; i < n1; i++) cin >> a[i];
for (int i = 0; i < n2; i++) cin >> b[i];
f[0] = 1;
for (int i = 0; i < n1; i++)
for (int j = a[i]; j <= m; j++)
{
f[j] += f[j - a[i]];
if (f[j] >= MOD) f[j] -= MOD;
}
for (int i = 0; i < n2; i++)
for (int j = m; j >= b[i]; j--)
{
f[j] += f[j - b[i]];
if (f[j] >= MOD) f[j] -= MOD;
}
cout << f[m] << endl;
return 0;
}
题目3
小a在玩一个很简单的游戏,游戏的内容是控制一个小人在一块矩形的空地内走,一旦小人走出矩阵范围,游戏就失败。游戏机上有上下左右四个键,每按一下小人就朝相应的方向走一步。这个游戏过于简单,小a说:“这种游戏我闭着眼睛玩都输不了”。于是他便闭上眼睛,进行一连串的操作。但若他中途输了的话就会停止。
那么问题来了:给定小a的操作,进行Q次询问,你能算出每次询问小人能走多少步吗?
输入描述:
第一行为长度为 L 的字符串 S,每个字符依次代表小a的一次操作。’u’代表向上,’d’代表向下,’l’代表向左,’r’代表向右。字符串 S 不会包含其他字符。
第二行是整数 Q,代表 Q 次询问。
接下来 Q 行,每行有四个整数:N,M,X,Y,保证 1≤X≤N,1≤Y≤M,矩阵的大小为 N∗M,小人初始位置为 (X,Y)。
对于 30% 的测试,0<X,Y,L,Q≤1000;
对于 100% 的测试,0<X,Y,L≤100000(1e5),o<Q≤30000(3e4);
输出描述:
每次询问要求你打印一个整数 s(单独一行),代表小人所走的步数。
样例:
输入:
uuurrdddddl
3
5 6 3 3
5 6 4 2
6 6 4 2
输出:
3
10
11
算法1
(二分查找) O(L+QlogL)
我们开4个数组 l[L],r[L],u[L],d[L],分别记录操作到每一步时,在四个方向上走到过的最大距离。
则四个数组都是单调递增的。
对于每个询问,我们先求出小人在四个方向上分别最多能走多远:LMax,RMax,UMax,DMax,则小人能走到第 i 步,当且仅当过程中没有出界,等价于:
l[i]≤LMax&&r[i]≤RMax&&u[i]≤UMax&&d[i]≤DMax
由于四个数组都是单调递增的,因此我们可以二分出最大步数。
时间复杂度分析:初始化要遍历整个操作序列,复杂度 O(L);对于每个询问,二分查找的复杂度是 O(logL),验证二分值是否合法的复杂度是 O(1),因此总时间复杂度是 O(L+QlogL).
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
const int N = 100010;
string ops;
int n, m, x, y;
int l[N], r[N], u[N], d[N];
bool check(int mid)
{
return l[mid] <= y - 1 && r[mid] <= m - y
&& d[mid] <= n - x && u[mid] <= x - 1;
}
int main()
{
cin >> ops;
for (int i = 0; i < ops.size(); i++)
{
char op = ops[i];
if (op == 'u') x--;
else if (op == 'd') x++;
else if (op == 'l') y--;
else y++;
l[i + 1] = max(l[i], -y);
r[i + 1] = max(r[i], y);
u[i + 1] = max(u[i], -x);
d[i + 1] = max(d[i], x);
}
int Q;
cin >> Q;
while (Q--)
{
cin >> n >> m >> x >> y;
if (!check(1))
{
cout << 0 << endl;
continue;
}
int L = 0, R = ops.size() - 1;
while (L < R)
{
int mid = L + R + 1 >> 1;
if (check(mid + 1)) L = mid;
else R = mid - 1;
}
cout << min((int)ops.size(), L + 2) << endl;
}
return 0;
}
算法2
(线性扫描) O(L+Q)
我们可以对算法1进行优化,在 l[L],r[L],u[L],d[L] 的基础上做进一步初始化,记录在每个方向上到达相应距离所需的最小长度。
对于每个询问,我们分别计算出在每个方向上走出界需要的最小长度,然后取个最小值即可。
复杂度分析:初始化复杂度 O(L),询问复杂度 O(Q),所以总时间复杂度是 O(L+Q)。
C++ 代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
const int N = 100010;
string ops;
int n, m, x, y;
int l[N], r[N], u[N], d[N];
int ln[N], rn[N], un[N], dn[N];
bool check(int mid)
{
return l[mid] <= y - 1 && r[mid] <= m - y
&& d[mid] <= n - x && u[mid] <= x - 1;
}
void init(int a[], int b[])
{
for (int i = 1, j = 1; i <= ops.size(); i++)
{
while (j <= ops.size() && a[j] < i) j++;
b[i] = j;
}
}
int main()
{
cin >> ops;
for (int i = 0; i < ops.size(); i++)
{
char op = ops[i];
if (op == 'u') x--;
else if (op == 'd') x++;
else if (op == 'l') y--;
else y++;
l[i + 1] = max(l[i], -y);
r[i + 1] = max(r[i], y);
u[i + 1] = max(u[i], -x);
d[i + 1] = max(d[i], x);
}
init(l, ln), init(r, rn), init(u, un), init(d, dn);
int Q;
cin >> Q;
while (Q--)
{
cin >> n >> m >> x >> y;
int t = ops.size();
int sl = y > t ? t : ln[y];
int sr = m - y + 1 > t ? t : rn[m - y + 1];
int su = x > t ? t : un[x];
int sd = n - x + 1 > t ? t : dn[n - x + 1];
cout << min(sl, min(sr, min(su, sd))) << endl;
}
return 0;
}
题目4
升序数组中第一个数是1,后续为若干连续的素数,对于数组里面的元素 m 和 n,(m<n) 都对应了一个有理数 m/n,现在给定这个数组和一个 K,要求返回第 K 小的有理数。
输入描述:
每组测试样本的输入格式为:
第一行是一个正整数 N;
从第二行开始,每行若干个正整数,依次存放 K,a1,a2,…an,一共有 N 行,也就是 N 组参数。
K 是输入参数,表示需要寻找的顺序第 K 小的有理数,a1 到 an 是以1开始的后续 n−1 个素数。
、
输出描述:
输出有 N 行,每行两个数字 m 和n,空格隔开,分别表示第 K 小有理数的分字和分母。
样例:
输入:
1
3 1 2 3 5
输出:
2 5
备注:
m,n 必须为 a1 到 an 中的满足条件的个数。
数据范围为:
10≤N≤20000
10≤K≤20000
1≤m<n≤20000
算法
(数据结构,堆) O(KlogN)
此题让求第 K 小数,可以利用堆排序的思想,每次弹出堆中最小数,弹 K 次即可。但如果直接用堆排序做,堆中元素有 O(N2) 个,建堆的时间复杂度和空间复杂度均是 N2=400000000=4∗108,会超时超内存。所以我们需要优化。
仔细思考我们会发现,求当前最小数时,我们不需要将每个分母的所有分子都放进堆里,只需放入该分母未使用过的最小分子即可。如果一个分母的分子被使用了,则将该分子分母从堆中弹出,再将该分母的下一个分子插入堆中。
时间复杂度分析:堆中元素一直维持在 N 个,总共需要 K 次弹出和插入操作,所以总时间复杂度是 O(KlogN)=20000∗log20000=260000。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <sstream>
#include <queue>
#define x first
#define y second
#define mp make_pair
using namespace std;
const int N = 20010, M = 1000000;
int a[N];
priority_queue<pair<double, pair<int, int>>> heap;
char buff[M];
int main()
{
int T, n, k;
cin >> T;
while (T--)
{
cin.getline(buff, M);
cin.getline(buff, M);
stringstream seq(buff);
n = 0;
seq >> k;
while (seq >> a[n]) n++;
for (int i = 0; i < n; i++)
heap.push(mp(-1.0 / a[i], mp(0, i)));
int up, down;
while (k--)
{
auto t = heap.top();
heap.pop();
heap.push(mp(a[t.y.x + 1] * -1.0 / a[t.y.y],
mp(t.y.x + 1, t.y.y)));
up = a[t.second.first];
down = a[t.second.second];
}
cout << up << ' ' << down << endl;
}
return 0;
}
题目5
有一台用电容组成的计算器,其中每个电容组件都有一个最大容量值(正整数)。
对于单个电容,有如下操作指令:
- 指令1:放电操作-把该电容当前电量值清零;
- 指令2:充电操作-把该电容当前电量补充到最大容量值;
- 指令3:转移操作-从电容 A 中尽可能多的将电量转移到电容 B ,转移不会有电量损失,如果能够充满 B 的最大容量,那剩余的电量仍然会留在 A 中。
现在已知有两个电容,其最大容量分别为 a 和 b,其初始状态都是电量值为 0,希望通过一些列的操作可以使其中某个电容(无所谓哪一个)中的电量值等于 c (c也是正整数),这一些列操作所用的最少指令条数记为 M,如果无论如何操作,都不可能完成,则定义此时 M=0。
显然对于每一组确定的 a,b,c,一定会有一个 M 与之对应。
输入描述:
每组测试样本的输入格式为:
第一行是一个正整数 N;
从第二行开始,每行都是 3 个正整数依次组成一组 a,b,c,一共有 N 组;
输出描述:
输出为 N 行,每行打印每一组对应的 M
样例
输入:
2
3 4 2
2 3 4
输出:
4
0
说明
对于 (3,4,2),最少只需要4个指令即可完成:
(设最大容量为 3 的是 A 号电容,另一个是 B 号电容)
操作:(充电 A)=> (转移 A -> B) => (充电 A)=> (转移 A -> B)。
对于 (2,3,4),显然不可能完成,输出0。
备注:
数据范围:
N:0<N<100
a,b,c:
0<a,b,c<105(50%)
0<a,b,c<107(30%)
0<a,b,c<109(20%)
算法
(扩展欧几里得算法,裴蜀定理) O(NlogV)
首先如果 c>a 并且 c>b 时,操作显然无法完成。所以我们只考虑 c≤Max(a,b) 的情况。
我们将两个电容看做一个整体,与外界只有两种操作:充电和放电。充电会使整个系统的总电量增加,放电会使整个系统的总电量减少。
我们先判断整个系统的总电量是否可以等于 c,即是否存在整数 x,y,使得 ax+by=c。如果 x,y 是负整数,则表示放电,如果是正整数,则表示充电。
根据裴蜀定理,等式可以满足当且仅当 gcd(a,b)|c。
我们现在来考虑当 gcd(a,b)|c 时,如何计算最少操作次数:
根据裴蜀定理,存在整数 x,y,使得 ax+by=c,且由于 c≤Max(a,b),所以 x,y 一定一正一负,或者一正一零。
我们现在来考虑怎么统计 ax+by=c 的操作次数。不妨假设 x>0,则整个操作流程如下:
- 给A充电;
- A转移到B;
- 如果B满电,则放空电量;
- 如果A非空,则重复2-4;
- 从1开始重复整个过程,直到A或B中的电量是 c为止。
如果 c>a,则最终会是B里先有 c 的电量(因为A存不下),此时有 x次充电,−y次放电,x−y次转移,共 2x−2y 次操作;
如果 c≤a,假设B中先有 c 的电量,则 a,b≥c,所以 b=c,仅需一次操作;假设A中先有 c 的电量,则有 x 次充电,x−y−1 次转移,−y−1次放电,共 2x−2y−2 次操作;
因此我们只需最小化 |x|+|y|。可以先用扩展欧几里得算法求出任意一组 (x,y),则
\\left\\{
\\begin{aligned}
x + kb/gcd(a,b) \\\\
y - ka/gcd(a,b)
\\end{aligned}
\\right.
, k = …, -2, -1, 0, 1, 2, …
就是该等式的全部整数解,我们找出和 0 最接近的两组 (x,y)(一组 x>0, 另一组 y>0 ),然后取操作次数较小的一组即可。
时间复杂度分析:扩展欧几里得算法的复杂度是 O(logV),一共 N 组测试数据,所以总时间复杂度是 O(NlogV)。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL gcd(LL a, LL b, LL &x, LL &y)
{
if (b == 0)
{
x = 1, y = 0;
return a;
}
LL q = gcd(b, a % b, y, x);
y -= a / b * x;
return q;
}
int main()
{
int T;
cin >> T;
while (T--)
{
LL a, b, c, x, y;
cin >> a >> b >> c;
int d = gcd(a, b, x, y);
if (c > a && c > b || c % d)
{
cout << 0 << endl;
continue;
}
if (c == a || c == b)
{
cout << 1 << endl;
continue;
}
if (y > 0) swap(x, y), swap(a, b);
LL a2 = a / d, b2 = b / d;
x *= c / d, y *= c / d;
LL k = x / b2;
x -= k * b2, y += k * a2;
LL res;
if (c > a) res = 2 * (x - y);
else res = 2 * (x - y - 1);
x -= b2, y += a2;
if (c > b) res = min(res, 2 * (y - x));
else res = min(res, 2 * (y - x - 1));
cout << res << endl;
}
return 0;
}
请问题目5里的
转移次数
具体是怎么计算得到的 ?tql,笔试也太难了,背包问题混合,数论!