大家好,我是白晨,一个不是很能熬夜,但是也想日更的人✈。如果喜欢这篇文章,点个赞,关注一下白晨吧!你的支持就是我最大的动力!
虽然还有很多课,但是也不能忘了写编程题呀。
这次白晨总结了一下大厂笔试时所出的经典题目,题型包括动态规划,数据结构,条件控制,数学归纳,回溯算法
等,难度比上周有很大的提高,有些题目非常巧妙,都是很有代表性的经典题目,适合大家复习和积累经验。
这里是第三周,大家可以自己先试着自己挑战一下,再来看解析哟!
⚔ 原题链接
:参数解析
这道题没有什么思想上的难度,核心就是分割字符串,主要是如何分割,这道题主要考验的是对于边界的控制能力,这里给出2种方法。
✈
法一
:
算法思想
:
直接利用输入时的空格来将字符串分割,我们天然的得到了一串串分割的字符串。不过,我们必须单独处理 ""
的字符串,以保证符合题意。
代码实现
:
#include
#include
#include
using namespace std;
int main()
{
vector<string> v;
string tmp;
while (cin >> tmp)
{
v.push_back(tmp);
}
int size = v.size();
for (int i = 0; i < v.size(); ++i)
{
// 找到"开头的字符串
if (v[i][0] == '"')
{
int j = i;
// 找到"结尾的字符串
for (; j < v.size(); ++j)
{
if (v[j][v[j].size() - 1] == '"')
{
break;
}
}
// 现在要合并""内的字符串
for (int k = i + 1; k <= j; ++k)
{
size--;
// 修补细节,在字符串间加空格,将其合并到v[i]这个串上,并且清空v[k]
v[i] += " ";
v[i] += v[k];
v[k] = "";
}
// 删除""
v[i].erase(v[i].begin());
v[i].erase(--v[i].end());
}
}
cout << size << endl;
for (auto str : v)
{
if (str != "")
cout << str << endl;
}
return 0;
}
✈
法二
:
算法思想
:
先读取一行,再分割字符串。
代码实现
:
#include
#include
#include
using namespace std;
int main()
{
string s;
getline(cin, s);
// 记录是否在""的指令中
int flag = 0;
// 记录当前的指令字符串
string tmp;
vector<string> v;
for (int i = 0; i < s.size(); ++i)
{
// 当是后"
if (flag)
{
// 如果遇到后",
if (s[i] == '"')
{
v.push_back(tmp);
flag = 0;
tmp.clear();
}
else
tmp += s[i];
}
else if (s[i] == ' ')
{
// 将当前指令插入后面
if (!tmp.empty())
v.push_back(tmp);
tmp.clear();
}
else if (s[i] == '"')
{
flag = 1;
}
else
{
tmp += s[i];
}
}
// 当前指令不为空,继续插入
if (!tmp.empty())
{
v.push_back(tmp);
}
cout << v.size() << endl;
for (auto& str : v)
{
cout << str << endl;
}
return 0;
}
⚔ 原题链接
:跳石板
动态规划经典题目,但是很多人可能第一眼会将其当成贪心算法的题目,贪心算法+回溯是可以得出答案的,但是可能不是最优解,这个大家可以自行验证。
算法思想
:
v[i]
—— 到达i
最少的跳跃次数。v[i]=INT_MAX
,INT_MAX为不可到达i
为num
的因数。v[m]
代码实现
:
#include
#include
#include
#include
using namespace std;
int main()
{
int n, m;
cin >> n >> m;
vector<int> v(m + 1, INT_MAX);
v[n] = 0;
int num = n;
while (num < m)
{
// 若此位置不可到达,直接跳过
if (v[num] == INT_MAX)
{
num++;
continue;
}
// 求num的因数
for (int i = 2; i <= sqrt(num); ++i)
{
// 当i为因数并且在num位置走i步不会超过最终目的地
if (num % i == 0 && num + i <= m)
{
v[num + i] = min(v[num + i], v[num] + 1);
}
// i为因数,说明num/i也为因数
int repair = num / i;
if (num % repair == 0 && num + repair <= m)
{
v[num + repair] = min(v[num + repair], v[num] + 1);
}
}
num++;
}
if (v[m] != INT_MAX)
cout << v[m] << endl;
else
cout << -1 << endl;
return 0;
}
⚔ 原题链接
:计算日期到天数转换
算法思想
:
主要考实现和边界控制,在二月上控制一下就可以。
代码实现
:
#include
using namespace std;
static int Month[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int main()
{
int _y, _m, _d;
cin >> _y >> _m >> _d;
int month = 1, day = 0;
while (month < _m)
{
if (month == 2 && (_y % 400 == 0 || _y % 100 != 0 && _y % 4 == 0))
{
day++;
}
day += Month[month++];
}
day += _d;
cout << day << endl;
return 0;
}
⚔ 原题链接
:幸运的袋子
算法思想
:
当两个正整数的和大于此两数的积时,至少一个数是1。
当正整数组的积已经大于正整数组的和,此时向正整数组中加入不为1的数,依然积大于和
,eg. 1* 2 * 3 * 4 > 1 + 2 + 3 + 4 ,此时加入一个2,1* 2 * 2 * 3 * 4 > 1 + 2 + 3 + 4 + 2 ,无法改变比较结果 代码实现
:
#include
#include
#include
using namespace std;
int getLuckyPocket(vector<int>& v, int pos, int add, int mul)
{
int cnt = 0;// 不同袋子数量
// 从pos位置开始遍历
for (int i = pos; i < v.size(); ++i)
{
add += v[i];
mul *= v[i];
// 当和大于积时
if (add > mul)
{
// 此时这种组合是可行的,+=1,再加上以这种组合前缀的所有组合满足题意的组合数
cnt += 1 + getLuckyPocket(v, i + 1, add, mul);
}
else if (v[i] == 1)
{
// 当1为第一个元素时,1*1==0+1,此时虽然不满足题意,但是接下来无论是什么数都满足题意
// 所以,cnt+= 以1为前缀满足题意的所有组合
cnt += getLuckyPocket(v, i + 1, add, mul);
}
else
// 如果add
// 直接退出即可
break;
// 状态回溯到开始状态
add -= v[i];
mul /= v[i];
// 跳过后面的相同元素
while (i < v.size() - 1 && v[i] == v[i + 1])
++i;
}
return cnt;
}
int main()
{
int n;
while (cin >> n)
{
vector<int> v(n);
for (int i = 0; i < n; ++i)
cin >> v[i];
sort(v.begin(), v.end());
cout << getLuckyPocket(v, 0, 0, 1) << endl;
}
return 0;
}
⚔ 原题链接
:查找输入整数二进制中1的个数
这是一道非常经典的题目了,这里我们直接上最优解法。
算法思想
:
n
。n &= (n - 1)
,这一步是整个算法的核心,这一步的意义是:每次必定消掉n最右边的1
。n == 0
,循环的次数就是二进制中1的个数 代码实现
:
int main()
{
int n;
while (cin >> n)
{
int cnt = 0;
while (n)
{
n &= n - 1;
cnt++;
}
cout << cnt << endl;
}
return 0;
}
⚔ 原题链接
:手套
这道题如果数学不好的话,真的会做的毫无头绪,并且牛客网的这个例子本来就很特殊,这更增加了这道题的难度。
算法思想
:
n
个颜色的小球,每种颜色的小球数量为 $最小覆盖数
,这也被称为 鸽巢原理
。最小覆盖数
,然后选出两个最小覆盖数中更小的最小覆盖数;最小覆盖数
呢? 代码实现
:
class Gloves {
public:
int findMinimum(int n, vector<int> left, vector<int> right) {
int left_min = 27, right_min = 27;
int lsum = 0, rsum = 0;
int sum = 0;
for (int i = 0; i < n; ++i)
{
if (left[i] != 0 && right[i] != 0)
{
// 当手套可以匹配时,选出左右手套的最小值
left_min = min(left[i], left_min);
right_min = min(right[i], right_min);
lsum += left[i];
rsum += right[i];
}
else
{
// 当有手套数量为0时,无论左右哪个覆盖数小,都必须将这个颜色的手套拿完
sum += left[i] + right[i];
}
}
// min(leftSum - leftMin + 1, rightSum - rightMin + 1)
lsum -= left_min;
rsum -= right_min;
// min(lsum, rsum) + 1 -- 最小覆盖数
// sum + 1 -- 这里必须要将数量为0的对应手套全拿光 同时再随机拿一个手套即可保证匹配
return min(lsum, rsum) + 1 + sum + 1;
}
};
⚔ 原题链接
:完全数计算
算法思想
:
没有什么特别难的,新手入门题目。
代码实现
:
#include
#include
using namespace std;
int main()
{
int n;
cin >> n;
int cnt = 0;
for (int i = 2; i <= n; ++i)
{
int sum = 1;
for (int j = 2; j <= sqrt(i); ++j)
{
if (i % j == 0)
{
sum += j + i / j;
}
}
if (sum == i)
cnt++;
}
cout << cnt << endl;
return 0;
}
⚔ 原题链接
:扑克牌大小
这道题同样是实现大于思想的题目,主要考察字符串分割以及条件控制,属于是对于控制考察的比较难的了。
算法思想
:
代码实现
:
#include
#include
#include
using namespace std;
static const string Hash[] = { "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2", "joker", "JOKER" };
int getPower(string& s)
{
for (int i = 0; i < 15; ++i)
{
if (Hash[i] == s)
return i;
}
return -1;
}
int main()
{
// v1为第一副牌,v2为第二副牌
vector<string> v1, v2;
string s;
getline(cin, s);
int begin = 0;
int i = 0;
// 在给定的字符串中,分割出第一手牌以及第二手牌
// 这里比较难的是分割边界控制
for (; i < s.size(); ++i)
{
if (s[i] == ' ')
{
v1.push_back(s.substr(begin, i - begin));
begin = i + 1;
}
if (s[i] == '-')
{
v1.push_back(s.substr(begin, i - begin));
begin = i + 1;
break;
}
}
i++;
for (; i < s.size(); ++i)
{
if (s[i] == ' ')
{
v2.push_back(s.substr(begin, i - begin));
begin = i + 1;
}
if (i == s.size() - 1)
{
v2.push_back(s.substr(begin, i - begin + 1));
}
}
// 当两副牌的数量相当时,可能的情况就是(两个人)单牌,对子,顺子,三个,炸弹,以及(一个人)大小王
// 这种情况下直接比较第一张牌就可以
if (v1.size() == v2.size())
{
if (getPower(v1[0]) > getPower(v2[0]))
{
for (auto& str : v1)
{
cout << str << " ";
}
}
else
{
for (auto& str : v2)
{
cout << str << " ";
}
}
}
// 当第一副牌为大小王时,第一副牌赢
else if (v1.size() == 2 && ((v1[0] == "joker" && v1[1] == "JOKER") || (v1[1] == "joker" && v1[0] == "JOKER")))
{
for (auto& str : v1)
{
cout << str << " ";
}
}
// 第二副牌为大小王时,第二副牌赢
else if (v2.size() == 2 && ((v2[0] == "joker" && v2[1] == "JOKER") || (v2[1] == "joker" && v2[0] == "JOKER")))
{
for (auto& str : v2)
{
cout << str << " ";
}
}
// 当第一副牌为炸弹时
else if (v1.size() == 4)
{
for (auto& str : v1)
{
cout << str << " ";
}
}
// 当第二副牌为炸弹时
else if (v2.size() == 4)
{
for (auto& str : v2)
{
cout << str << " ";
}
}
// 无法比较
else
{
cout << "ERROR" << endl;
}
return 0;
}
⚔ 原题链接
:杨辉三角的变形
算法思想
:
白晨的第一想法就是动态规划对杨辉三角变式进行模拟
,直接莽夫,先保证过了就行。
#include
#include
using namespace std;
int main()
{
int n;
cin >> n;
vector<vector<int>> v(n);
for (int i = 0; i < n; ++i)
{
v[i].resize(2 * i + 3, 0);
v[i][0] = v[i][2 * i] = 1;
}
for (int i = 1; i < n; ++i)
{
for (int j = 1; j < 2 * i + 1; ++j)
{
if (j - 2 >= 0)
{
v[i][j] = v[i - 1][j - 2] + v[i - 1][j - 1] + v[i - 1][j];
}
else
{
v[i][j] = v[i - 1][j - 1] + v[i - 1][j];
}
}
}
for (int i = 1; i < 2 * n - 1; ++i)
{
if (v[n - 1][i] % 2 == 0)
{
cout << i + 1 << endl;
return 0;
}
}
cout << -1 << endl;
return 0;
}
上面就是第一次白晨动态模拟实现的杨辉三角,但是我忽略了n过大的情况,比如n=10000,就已经超出了可申请空间的范围,所以我又做了以下优化:
#include
#include
using namespace std;
int main()
{
int n;
cin >> n;
vector<vector<int>> v(2, vector<int> (n + 1));
v[0][0] = v[1][0] = 1;
for (int i = 1; i < n; ++i)
{
if (i % 2 == 1)
{
for (int j = 1; j < i + 1; ++j)
{
if (j - 2 < 0)
{
v[0][j] = v[1][j - 1] + v[1][j];
}
else if (j == i)
{
v[0][j] = 2 * v[1][j - 2] + v[1][j - 1];
}
else
{
v[0][j] = v[1][j - 2] + v[1][j - 1] + v[1][j];
}
}
}
else
{
for (int j = 1; j < i + 1; ++j)
{
if (j - 2 < 0)
{
v[1][j] = v[0][j - 1] + v[0][j];
}
else if (j == i)
{
v[1][j] = 2 * v[0][j - 2] + v[0][j - 1];
}
else
{
v[1][j] = v[0][j - 2] + v[0][j - 1] + v[0][j];
}
}
}
}
if (n % 2 == 0)
{
for (int i = 1; i < n; ++i)
{
if (v[0][i] % 2 == 0)
{
cout << i + 1 << endl;
return 0;
}
}
}
else
{
for (int i = 1; i < n; ++i)
{
if (v[1][i] % 2 == 0)
{
cout << i + 1 << endl;
return 0;
}
}
}
cout << -1 << endl;
return 0;
}
但是还是哒咩,这一次是n=100000的时候超时了。所以,模拟的方法是彻底行不通了,我们现在要试着找规律了。
前十行的数据:
2
个位置上。3
个位置;4
个位置。 代码实现
:
#include
using namespace std;
int main()
{
int n;
cin >> n;
if (n <= 2)
cout << -1 << endl;
else if (n % 2 == 1)
cout << 2 << endl;
else
{
if (n % 4 == 0)
cout << 3 << endl;
else
cout << 4 << endl;
}
return 0;
}
⚔ 原题链接
:二叉树的镜像
这道题其实就是反转二叉树的套皮题目,只用理解如何反转一棵二叉树,就能理解这道题。
算法思想
:
这里我们使用使用分治的思想:
NULL
;不为空,执行以下操作。上面的过程可以类似的视为前序遍历的过程。
代码实现
:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
if(root == nullptr)
return nullptr;
TreeNode* left = mirrorTree(root->left);
TreeNode* right = mirrorTree(root->right);
root->left = right;
root->right = left;
return root;
}
};
⚔ 原题链接
:统计每个月兔子的总数
算法思想
:
时间/月份 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
兔子数量 | 1 | 1 | 2 | 3 | 5 | 8 | 13 |
代码实现
:
#include
using namespace std;
int main()
{
int f1 = 1, f2 = 1;
int n;
cin >> n;
for(int i = 3; i <= n; ++i)
{
int tmp = f1 + f2;
f1 = f2;
f2 = tmp;
}
cout << f2 << endl;
return 0;
}
⚔ 原题链接
:字符串通配符
⚾
法一
:递归+记忆化搜索
算法思想
:
s1
为带有通配符的串,s2
为待匹配的串。
具体思路见代码
代码实现
:
#include
#include
#include
using namespace std;
bool is_match(const char* s1, const char* s2)
{
// 当s2已经结束
if (*s2 == '\0')
{
// 如果s1也已经结束,返回真
if (*s1 == '\0')
{
return true;
}
else
{
int i = 0;
// 如果s1后面还有*,则将*跳过再判断是不是结束
while (s1[i] == '*')
i++;
if (s1[i] == '\0')
return true;
else
return false;
}
}
// 如果s1先结束,说明不匹配
else if (*s1 == '\0')
{
return false;
}
// 当s1遇到?时,如果s2对应的字符为数字或者字母时,可以继续向前匹配
if (*s1 == '?' && isalnum(*s2))
{
return is_match(s1 + 1, s2 + 1);
}
// 当s1遇到*时
else if (*s1 == '*' && isalnum(*s2))
{
int i = 0;
// 跳过连续的*
while (s1[i] == '*')
i++;
// 当下面的匹配条件任意满足,就能说明为真,这一步是本代码的灵魂,但是很难想到
return is_match(s1 + i - 1, s2 + 1) || is_match(s1 + i, s2) || is_match(s1 + i, s2 + 1);
}
// 当s1,s2对应都是字符时,进行比较
else if (*s1 == *s2 || *s1 + 32 == *s2 || *s1 == *s2 + 32)
{
return is_match(s1 + 1, s2 + 1);
}
return false;
}
int main()
{
string s1, s2;
cin >> s1 >> s2;
bool match = is_match(s1.c_str(), s2.c_str());
if (match == true)
cout << "true" << endl;
else
cout << "false" << endl;
return 0;
}
⚾
法二
:动态规划
算法思想
:
带通配符的字符串匹配:动态规划
v[i][j] —— s1的前i个字符是否匹配s2的前j个字符
v[0][0]=true
,对于s1一开始为*
的情况,v[i][0]=0
,其余都设为falsev[s1.size()][s2.size()]
代码实现
:
#include
#include
#include
#include
using namespace std;
int main()
{
string s1, s2;
cin >> s1 >> s2;
// 多开辟一行一列,方便判断
vector<vector<bool>> v(s1.size() + 1, vector<bool>(s2.size() + 1));
v[0][0] = true;
// 对于前几个字符就是*的要做特殊处理
for (int i = 0; s1[i] == '*'; ++i)
{
v[i + 1][0] = true;
}
for (int i = 1; i < v.size(); ++i)
{
for (int j = 1; j < v[0].size(); ++j)
{
// s1[i-1] == '?'时,此时可以跳过一个数字或字母字符
if (s1[i - 1] == '?' && isalnum(s2[j - 1]))
// 此时的状态转移如下:
v[i][j] = v[i - 1][j - 1];
// 为*时,只要v[i - 1][j - 1] || v[i][j - 1] || v[i - 1][j]任意有一个为真即可为真
// 与上一个方法有异曲同工之妙
else if (s1[i - 1] == '*' && isalnum(s2[j - 1]))
v[i][j] = v[i - 1][j - 1] || v[i][j - 1] || v[i - 1][j];
else
// s1[i-1]与s2[j-1]都为字符时,直接比较
v[i][j] = v[i - 1][j - 1] && (s1[i - 1] == s2[j - 1] || s1[i - 1] + 32 == s2[j - 1] || s1[i - 1] == s2[j - 1] + 32);
}
}
if (v[s1.size()][s2.size()])
cout << "true" << endl;
else
cout << "false" << endl;
return 0;
}
本次题目难度有了很大提高,特别是动态规划以及数学归纳
题目的难度,本次增加了不同动态规划的题目,比如:一维数组,二维矩阵,有助于大家提高应对动态规划题目的能力。
对于数学归纳的能力也有很强的考察,相信大家做完会有所收获。
这个是一个新的系列——《笔试经典编程题目》,隶属于【刷题日记】系列,白晨开这个系列的目的是向大家分享经典的笔试编程题,以便于大家参考,查漏补缺以及提高代码能力。如果你喜欢这个系列的话,不如关注白晨,更快看到更新呀。
本文是笔试经典编程题目的第三篇,如果喜欢这个系列的话,不如订阅【刷题日记】系列专栏,更快看到更新哟
如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。
如果大家喜欢这个系列,还请大家多多支持啦!
如果这篇文章有帮到你,还请给我一个大拇指
和小星星
⭐️支持一下白晨吧!喜欢白晨【算法】系列的话,不如关注
白晨,以便看到最新更新哟!!!
我是不太能熬夜的白晨,我们下篇文章见。