今日头条2018春季校园招聘研发岗位笔试编程题 - 题解
由于24号头条笔试我还在回学校的路上,因此没有在笔试中做题,这些题目都是后来同学给我的,我做了下,不保证100%通过,因为我自己没在笔试中提交过。如果题目抄写有误或者说我的解法有误,或者你有更好的解法都可以在评论区留言讨论。
头条这次的题难度中等,基本上都能想到思路(如果你看到这五个题目没有思路就要好好练练了),而且实现出来都能通过(如果你想到思路但是不能实现出来,那么编码能力还要加强)。这场题的亮点在于考虑问题要全面。
第一题:
题目:
在
n
个元素的数组中,找到差值为k
的数字对去重后的个数。
输入描述:
第一行,
n
和k
,n
表示数字个数,k
表示差值
第二行,n
个正整数
输出描述:
差值为
k
的数字对去重后的个数
样例
in: 5 2 1 5 3 4 2 out: 3
解析
首先要对数组元素去重,这里用unique
函数,这个函数会把重复的元素移到数组的后半部分,然后枚举不重复的部分,二分查找后面的元素,这样可以解决这个问题。
去重的基本操作是:
sort(arr.begin(), arr.end());
arr.erase(unique(arr.begin(), arr.end()), arr.end());
有两个点需要注意一下,
第一,k
可能为0,因此要特殊处理下。
第二,题中对k
的描述是整数,那么可能出现负值,故要对k
取绝对值abs(k)
。
代码:
#include <bits/stdc++.h>
using namespace std;
int main()
{
for (int n, k, ans = 0; cin >> n >> k; ans = 0) {
vector<int> arr;
for (int i = 0, x; i < n; cin >> x, arr.push_back(x), ++i) {}
if (!k) {
map<int, int> mp;
for (int i = 0; i < n; mp[arr[i++]]++) {}
for (auto it = mp.begin(); it != mp.end(); ans += it++->second > 1) {}
} else {
sort(arr.begin(), arr.end());
arr.erase(unique(arr.begin(), arr.end()), arr.end());
for (auto it = arr.begin(); it != arr.end(); ++it)
ans += binary_search(it, arr.end(), *it + abs(k));
}
cout << ans << endl;
}
return 0;
}
听说上面用二分做不能过全部的数据,时间复杂度都不能过,那么就说明卡常数了,因此这里再次使用剑指Offer66题之每日6题 - 第七天中第六题:和为S的两个数字中的头尾指针法,这里变形为差为K的两个数字。其实是一样的,这样一来,时间复杂度就为,但是排序的时间复杂度还是,所以感觉没什么用,还是给出代码吧,到时候看看能不能过。
#include <bits/stdc++.h>
using namespace std;
int main()
{
for (int n, k, ans = 0; cin >> n >> k; ans = 0) {
vector<int> arr;
for (int i = 0, x; i < n; cin >> x, arr.push_back(x), ++i) {}
if (!k) {
map<int, int> mp;
for (int i = 0; i < n; mp[arr[i++]]++) {}
for (auto it = mp.begin(); it != mp.end(); ans += it++->second > 1) {}
} else {
sort(arr.begin(), arr.end());
arr.erase(unique(arr.begin(), arr.end()), arr.end());
for (int head = 0, tail = 1; tail < (int)arr.size(); head++) {
for (; tail < (int)arr.size() && arr[tail] - arr[head] < k; ++tail) {}
if (tail >= (int)arr.size())
break;
ans += arr[tail] - arr[head] == abs(k);
}
}
cout << ans << endl;
}
return 0;
}
第二题:
题目:
定义两个字符串变量:
s
和m
,再定义两种操作,
第一种操作:
m = s
;s = s + s
;
第二种操作:
s = s + m
;
假设s
,m
初始化如下:
s = "a"
;m = s
;
求最小的操作步骤数,可以将s
拼接到长度等于n
输入描述:
一个整数
n
,表明我们需要得到s
字符长度,
输出描述:
一个整数,表明总共操作次数
样例
in: 6 out: 3
解析
很明显的广搜题,最小值问题,这个广搜给大家讲**意的地方,由于s
长度不会减少,且m
的长度也不会减少,所以,在的过程中不需要判重,而且,如果只是对s
的长度判重还会导致答案错误,为什么呢,因为对于一个特定的长度,可以有很多方式到达,尽管有些方式到达时经历了很多步骤,但是这是必须的,因为对于同一s
长度n
,它的m
的长度不同,也就意味着下一步骤可以生成不同长度的s
(有一些长度的s
,比如5,只能由s = s + m
, |s| = 4, |m| = 1
生成),因此,对于每个状态,需要两个变量表示,(|s|,|m|)
。很基础的入门题;
但是,我用广搜做,然后打印出1 - 10000
的所有答案时,发现程序根本结束不了,因为没有预处理,没有利用之前的结果去计算后面的输入,预处理过后就不会超时了,这里给出代码。
#include <bits/stdc++.h>
using namespace std;
struct Node {
int s, m, cnt;
Node() {}
Node(int s, int m, int cnt) : s(s), m(m), cnt(cnt) {}
};
#define MAX_SIZE 10000
int main()
{
vector<int> ans(MAX_SIZE, -1);
queue<Node> que;
que.emplace(1, 1, 0);
while (!que.empty()) {
auto now = que.front();
que.pop();
if (ans[now.s] == -1)
ans[now.s] = now.cnt;
if (now.s * 2 < MAX_SIZE)
que.emplace(now.s * 2, now.s, now.cnt + 1);
if (now.s + now.m < MAX_SIZE)
que.emplace(now.s + now.m, now.m, now.cnt + 1);
}
for (int n; cin >> n; cout << ans[n] << endl) {}
return 0;
}
这个题动态规划也很好搞。
首先,要知道,m
的长度只可能在第一种操作中变化,而且m
的长度始终小于等于s
长度的一半;
由上面的分析得,dp[i][j]
表示s
的长度为i
,m
的长度为j
时所需要的最少步数。那么先枚举第二种情况,s = s + m
,即:
for (int j = 1; j <= i / 2; j++)
dp[i][j] = min(dp[i][j], dp[i - j][j] + 1);
如果i
为偶数则继续枚举第一种情况,即:
for (int j = 1; j <= i / 4; j++)
dp[i][i / 2] = min(dp[i][i / 2], dp[i / 2][j] + 1);
最后在dp[i][j]
中预处理出s
的长度为i
时所需要的最小步数,即:
vector<int> ans(MAX_SIZE);
for (int i = 1; i < MAX_SIZE; i++)
ans[i] = *min_element(dp[i] + 1, dp[i] + MAX_SIZE / 2);
如果有同学用通过了这题,那只能说明后台数据太水了。
我收回上面这句话,我之所以这么说是因为看到很多同学的代码都没有预处理,如果后台数据是1 - 10000
,那么很多同学的代码确实会超时。
代码
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE 10000
int dp[MAX_SIZE][MAX_SIZE >> 1];
int main()
{
for (int i = 2; i < MAX_SIZE; i++) {
for (int j = 1; j < MAX_SIZE / 2; j++)
dp[i][j] = 0x3f3f3f;
for (int j = 1; j <= i / 2; j++)
dp[i][j] = min(dp[i][j], dp[i - j][j] + 1);
if (i % 2)
continue;
for (int j = 1; j <= i / 4; j++)
dp[i][i / 2] = min(dp[i][i / 2], dp[i / 2][j] + 1);
}
vector<int> ans(MAX_SIZE);
for (int i = 1; i < MAX_SIZE; i++)
ans[i] = *min_element(dp[i] + 1, dp[i] + MAX_SIZE / 2);
for (int n; cin >> n; cout << ans[n] << endl) {}
return 0;
}
还有同学和我反应,二维动态规划可能会超内存,分析一下,二维数组总共的内存是,确实有可能会超内存,那么只能降维变成一维动态规划。
如何做呢,如果你做过650. 2 Keys Keyboard,那么就知道如何做了,其实你把这一题的背景看成LeetCode上这题的背景,这个一维动态规划很好想,LeetCode上面复制和粘贴是两步操作,而这一题,复制的同时必须粘贴,因此只需要把LeetCode的代码稍微修改一下就能通过了,这里要注意,LeetCode上说,这题可以贪心,因此时间复杂度可以达到,但是我不能证明这种贪心的正确性,虽然贪心能过题,因此这里我还是给出了dp
的代码。
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE 10000
int dp[MAX_SIZE];
int main()
{
// init
for (int i = 2; i < MAX_SIZE; i++) {
dp[i] = i - 1;
for (int j = i / 2; j >= 1; j--)
if (i % j == 0)
dp[i] = min(dp[i], dp[j] + i / j - 1);
}
// work
for (int n; cin >> n; cout << dp[n] << endl) {}
return 0;
}
如果这个题把第一步操作改成m = m + m
,初始化改为s = "", m = "a"
,其余不变,那么该怎么写呢,可以思考一下最好的解法。提示一下,使用二进制。
第三题:
题目:
今日头条6周年周年庆就要开始啦。活动主办方请你帮忙制作一个小彩蛋。你的程序需要读取一个表达式,并输出用字符
6
拼出的计算结果。相邻数字使用两个英文句号"."
间隔,如下是0123456789
。66666......6..66666..66666..6...6..66666..66666..66666..66666..66666 6...6......6......6......6..6...6..6......6..........6..6...6..6...6 6...6......6..66666..66666..66666..66666..66666......6..66666..66666 6...6......6..6..........6......6......6..6...6......6..6...6......6 66666......6..66666..66666......6..66666..66666......6..66666..66666
输入描述:
第一行为一个整数
n
接下来n
行,每一行为一个表达式
对于30%的数据,表达式仅包含'6', '+', '-'
三种字符
对于100%的数据,表达式仅包含'6', '+', '-', '*'
四种字符。,表达式长度不超过100,其中'+', '-', '*'
均为二元运算符,计算中间结果在之内,最终结果在
输出描述:
对于每组数据输出用字符
6
拼出的计算结果。
样例
in: 2 6+6 6*6 out: ....6..66666 ....6......6 ....6..66666 ....6..6.... ....6..66666 66666..66666 ....6..6.... 66666..66666 ....6..6...6 66666..66666
解析
这还是个模拟题,考编码的熟练度和准确度。
但是要注意一点,表达式中可能出现66*66
这种情况,不要被样例误导了。
如果要全部通过,那么就要使用后缀表达式求值的方法,这样可以解决表达式中运算符的优先级问题,后缀表达式求值这里不多赘述,可以去学学代码怎么写。
// 后缀表达式求值
stack<char> st_ope;
stack<LL> st_digit;
vector<char> ope;
int sum = 0;
for (auto it = str.begin(); it != str.end(); ++it)
if (*it == '6')
sum = sum * 10 + 6;
else {
st_digit.push(sum);
sum = 0;
if (*it == '*')
st_ope.push('*');
else {
for (; !st_ope.empty() && st_ope.top() == '*';
ope.push_back(st_ope.top()), st_ope.pop()) {}
st_ope.push(*it);
}
}
for (st_digit.push(sum); !st_ope.empty(); ope.push_back(st_ope.top()), st_ope.pop()) {}
然后就是把表达式的结果化成字符串:
LL digit = st_digit.top();
vector<int> nums;
for (; digit; nums.push_back(digit % 10), digit /= 10) {}
reverse(nums.begin(), nums.end());
最后就是用对应的数字找到对应的字符画:
const char digit2string[RAW][COL * 10 + 1] = {
"66666....666666666666...66666666666666666666666666",
"6...6....6....6....66...66....6........66...66...6",
"6...6....66666666666666666666666666....66666666666",
"6...6....66........6....6....66...6....66...6....6",
"66666....66666666666....66666666666....66666666666"
};
for (int i = 0; i < RAW; i++) {
for (int j = 0; j < (int)nums.size(); j++) {
for (int k = 0; k < COL; k++)
cout << digit2string[i][nums[j] * COL + k];
cout << (j == (int)nums.size() - 1 ? "\n" : "..");
}
}
这题没有什么好讲的,好好看看代码,学学技巧就行了。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define RAW 5
#define COL 5
const char digit2string[RAW][COL * 10 + 1] = {
"66666....666666666666...66666666666666666666666666",
"6...6....6....6....66...66....6........66...66...6",
"6...6....66666666666666666666666666....66666666666",
"6...6....66........6....6....66...6....66...6....6",
"66666....66666666666....66666666666....66666666666"
};
int main()
{
int T;
for (cin >> T; T--; ) {
string str;
cin >> str;
stack<char> st_ope;
stack<LL> st_digit;
vector<char> ope;
int sum = 0;
for (auto it = str.begin(); it != str.end(); ++it)
if (*it == '6')
sum = sum * 10 + 6;
else {
st_digit.push(sum);
sum = 0;
if (*it == '*')
st_ope.push('*');
else {
for (; !st_ope.empty() && st_ope.top() == '*';
ope.push_back(st_ope.top()), st_ope.pop()) {}
st_ope.push(*it);
}
}
for (st_digit.push(sum); !st_ope.empty(); ope.push_back(st_ope.top()), st_ope.pop()) {}
for (int i = 0; st_digit.size() >= 2; ) {
LL d1 = st_digit.top();
st_digit.pop();
LL d2 = st_digit.top();
st_digit.pop();
LL ret = 0;
switch (ope[i++])
{
case '-':
ret = d1 - d2;
break;
case '+':
ret = d1 + d2;
break;
case '*':
ret = d1 * d2;
break;
}
st_digit.push(ret);
}
LL digit = st_digit.top();
vector<int> nums;
for (; digit; nums.push_back(digit % 10), digit /= 10) {}
reverse(nums.begin(), nums.end());
for (int i = 0; i < RAW; i++) {
for (int j = 0; j < (int)nums.size(); j++) {
for (int k = 0; k < COL; k++)
cout << digit2string[i][nums[j] * COL + k];
cout << (j == (int)nums.size() - 1 ? "\n" : "..");
}
}
}
return 0;
}
第四题:
题目:
给一个包含
n
个整数元素的集合a
,一个包含m
个整数元素的集合b
。
定义magic操作为,从一个集合中取出一个元素,放到另一个集合里,切操作过后每个集合的平均值都大于操作前。
注意一下两点:
- 不可以把一个集合的元素取空,这样就没有平均值了
- 值为
x
的元素从集合b
取出放入集合a
,但集合a
中已经有值为x
的元素,则a
的平均值不变(因为集合元素不会重复),b
的平均值可能会改变(因为x
被取出了)
问最多可以进行多少次magic操作?
输入描述:
第一行为两个整数
n,m
第二行n
个整数,表示集合a中的元素
第三行m
个整数,表示集合b中的元素
对于100%的数据,,集合a
中元素互不相同,集合b
中的元素互不相同。
输出描述:
输出一个整数,表示最多可以进行的操作次数。
样例
in: 3 5 1 2 5 2 3 4 5 6 out: 2
解析
模拟题,题中明确说了,magic后两个集合的平均值都要大于之前,这就要求,取数只能取小于平均值的那些元素,放数只能放大于该集合平均值的数字,很好理解,比如你们班的数学成绩平均分60分,那么进来一个插班生,他的成绩你觉得是多少才能不拉低平均分呢,必然是60分以上,如果要走一个人,那么肯定也是让小于平均分的那个学生走;
上面解决了第一个问题,取哪些数字和放哪些数字,现在问题就变成了从哪个集合中取,又放到哪个集合中,很明显,肯定是从平均值大的集合中取小于该集合平均值且大于令一个集合中的平均值的这些元素放到另一个集合中,这样是不是很合理;
这又解决了一个问题,那么最后一个问题就是什么样取的顺序能使magic进行的次数尽可能多,肯定是从小的开始取,因为符合要求的元素(从平均值大的集合中取小于该集合平均值且大于令一个集合中的平均值的这些元素放到另一个集合中)中从小到大开始取,假设a
集合的平均值大,b
的小,a
中符合要求的最小的元素一定靠b
集合的平均值近,离a
集合的平均值远,那么,这样就会造成a
集合丢掉了一个最垃圾的元素,从而使a
集合的平均值增长地很多,而b
集合放入了一个和平均值差不多的元素,那么会使b
集合的平均值增幅不明显,这样子就会使a
集合的平均值永远大于b
集合的平均值,所以可以使得更多符合要求的元素可以从a
集合放入b
集合。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int main()
{
for (int n, m; cin >> n >> m; ) {
set<LL> A, B;
double aveA = 0, aveB = 0;
for (int i = 0, x; i < n; A.insert((cin >> x, x)), aveA += x, ++i) {}
for (int i = 0, x; i < m; B.insert((cin >> x, x)), aveB += x, ++i) {}
// aveA / n > aveB / m;
if (aveA / n < aveB / m) {
A.swap(B);
swap(aveA, aveB);
swap(n, m);
}
int ans = 0;
for (auto it = A.begin(); it != A.end() && *it * 1.0 < aveA / n; ++it) {
if (B.find(*it) != B.end())
continue;
if (*it > aveB / m) {
aveA -= *it, aveB += *it;
--n, ++m;
++ans;
}
}
cout << ans << endl;
}
return 0;
}
第五题:
题目:
小T最近迷上一款跳板小游戏
已知空中有N
个高度互不相同的跳板,小T刚开始在高度为0
的地方,每次跳跃可以选择与自己高度绝对值小于等于H
的跳板,跳跃过后到达以跳板为轴的镜像位置,问小T在最多跳K
次的情况下最高能跳多高?(任意时刻,高度不能为负)
输入描述:
第一行三个整数
N,K,H
以下N行,每行一个整数,表示第i
个跳板的离地高度
输出描述:
一个整数,表示最高能跳到的高度。
样例
in: 3 3 2 1 3 6 out: 8
解析
求解最小值问题无非就是三种方法,动态规划和和二分答案。这里选择,因为动态规划不适合,不满足无后效性,由于题中也没给出数据范围,因此用还是很危险的。
这个很好写,当前状态只能借助与跳板高度绝对值小于等于k
的跳板,这里遍历就好,但是要清楚一点,理论上最高能跳max{height} + k
,也就是说,借助最高的一块跳板再跳k
米。
代码
#include <bits/stdc++.h>
using namespace std;
int main()
{
for (int n, k, h; cin >> n >> k >> h; ) {
vector<int> height;
for (int i = 0, x; i < n; height.push_back((cin >> x, x)), ++i) {}
int TOP = *max_element(height.begin(), height.end()) + h + 1;
vector<bool> used(TOP, false);
queue<pair<int, int> > que;
used[0] = true;
que.emplace(0, 0);
int ans = 0;
for (; !que.empty(); ) {
auto now = que.front();
que.pop();
ans = max(now.first, ans);
for (auto it = height.begin(); it != height.end(); ++it) {
if (abs(now.first - *it) <= h &&
(2 * *it - now.first > 0 && 2 * *it - now.first < TOP) &&
!used[2 * *it - now.first] && now.second < k) {
used[2 * *it - now.first] = true;
que.emplace(2 * *it - now.first, now.second + 1);
}
}
}
cout << ans << endl;
}
return 0;
}
上一篇: J2ME解析并读取xml文件
下一篇: J2ME的写文件操作