[卡码网 · 第五期模拟面试] 算法题解

327 阅读4分钟

[卡码网 · 第五期模拟面试] 算法题解

因为要准备面试算法,菜鸟虽然在力扣刷了近两百题,但是是第一次写算法题解,如果哪些地方解释的不清楚,或者哪些地方写错了,欢迎指出,我会努力修正继续进步的!!谢谢大家!!!

卡码网网址:kamacoder.com/

42. 路径简化

题目描述

假设你正在编写一个简单的 Unix 命令行模拟器,用户可以使用该模拟器来导航文件系统。用户可以输入 cd 命令来更改当前工作目录,并使用 pwd 命令来查看当前工作目录的路径。

但是,用户可能会输入复杂的路径,包括"/../"、"//"、"/./"或者多个连续的"/"等冗余部分,这会导致路径不太直观和容易理解。因此,你需要实现一个简化路径的功能,以确保路径始终保持干净、规范化和易于理解。

每次的 cd 命令都在根目录下进行执行。测试数据中不包含pwd命令。

输入

输入包含多组测试数据,每组测试数据有一个字符串,表示用户命令。

输出

输出简化后的路径,且路径不能以 "/" 结尾。

样例输入

cd /a/./b/../../c/
cd /abc/def/ghi//////jkl/./mno/../pqr/

样例输出

/c
/abc/def/ghi/jkl/pqr

提示

命令字符串长度小于1000。

简化路径时,要确保路径的开头以斜杠"/"开头,路径中不含多个连续的斜杠"//",且路径不以斜杠"/"结尾。

思路:

  • 用C++的朋友首先要注意这个不能用 scanf cin 进行输入,因为中间含有空格,需要一整行一整行的进行读取(调试的时候才发现我这里写错了,浪费起码十分钟),需要使用getline(cin, str)。
  • 其次最佳的解法应该是使用数据结构中的来解,虽然本人当时意识到了栈又方便又不容易出错,但抱着for循环已经写完并且辛辛苦苦写了一大堆的条件判断后,而且过了样例,就将错就错下去了,后面写了快四十分钟还是一直WA,果断跳题。
  • 正确的解法应当是:
    • 使用栈来存储剩余的字符;
    • 遇到 斜杠/就进行判断后压栈,通过栈顶是否为斜杠来避免压入过多的斜杠;
    • 遇到字母压栈;
    • 遇到一个点 . 直接跳过,遇到两个点 .. 则一直弹栈直到下一个栈顶是斜杠 /

踩坑点:

  1. 原本以为/是根目录了,没想到还能够../,缺乏对这种情况的处理导致一直报错。

image-20230908105658372.png

  1. 查看用例,麻了,彻底麻了,看题目里面以为/..//./里面的点都是被包裹在双斜杠里面的,没想到还是被摆了一道。

image-20230908110306041.png

题解代码:

感谢群友的提醒,给了我更好优雅的解答方法,此题对应力扣71题。

#include <iostream>
#include <stack>
#include <sstream>
#include <algorithm>
using namespace std;

string str;

int main()
{
	while (getline(cin, str))
	{
		stringstream ss;
		
		// 去掉前面三个字符cd和空格
		str = str.substr(3);
		ss << str;
		stack<string> st;
		
		// 用 "/" 来分隔字符串
		while (getline(ss, str, '/')){
		    
		    //如果str为"..",表示上一级,就出栈一次
			if (str == ".."){
				if (!st.empty())
					st.pop();
				continue;
			}
			
			// "."表示当前目录,可以直接跳过
			else if (str == "." || str.empty())
				continue;
				
			// 其它则直接压入栈中
			else
				st.push(str);
		}

		string res;
		
		while (!st.empty()){
			res = "/" + st.top() + res;
			st.pop();
		}
		
		// 注意判断res是否为空
		if (res.empty())
			res = "/";
		cout << res << endl;
	}
	return 0;
}

44. 开发商购买土地

题目描述

在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。

现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。

然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。 为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。

注意:区块不可再分。

输入

第一行输入两个正整数,代表 n 和 m。

接下来的 n 行,每行输出 m 个正整数。

输出

请输出一个整数,代表两个子区域内土地总价值之间的最小差距。

样例输入

3 3
1 2 3
2 1 3
1 2 3

样例输出

0

提示

如果将区域按照如下方式划分:

1 2 | 3 2 1 | 3 1 2 | 3

两个子区域内土地总价值之间的最小差距可以达到 0。

数据范围:

1 <= n, m <= 100; n 和 m 不同时为 1。

思路:

首先想到的确实是前缀和,但是没做过这种二维形式的前缀和,虽然有个大概的思路,还是没有做出来,最后看解题代码总结如下:

  • 主要的知识点是前缀和矩阵,需要有二维前缀和的基础(我也是边写博客边补的)

  • 要理解区域划分的意思,只能横切和纵切,不能切出下图所示的这两种切法,不然前缀和的意义也没了

image-20230908162330359-16941614123981.png

  • 计算好二维前缀和数组之后,进行一行一行和一列一列的查找即可

题解代码:

#include <iostream>
using namespace std;

const int N = 110;

int n, m;
int a[N][N], s[N][N]; // a是初始数组,s是前缀和数组

int main()
{
    int res = 0x7fffffff;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
        {
            // 两个数组的初始化,可以写在同一个循环里面,前缀和数组的求解只依赖已经赋值的数组
            scanf("%d", &a[i][j]);
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
        }
    
    // 特判为1的情况
    if (n == 1 && m == 1)
    {
        printf("%d\n", a[1][1]);
        return 0;
    }
    
    // 一行一行的比较
    for (int i = 1; i <= n; ++i)
    {
        res = min(res, abs(s[i][m] - (s[n][m] - s[i][m])));
    }
    
    // 一列一列的比较
    for (int i = 1; i <= m; ++i)
    {
        res = min(res, abs(s[n][i] - (s[n][m] - s[n][i])));
    }
    printf("%d\n", res);
    return 0;
}

45 虚拟棋盘对战

题目描述

两个玩家在一款虚拟棋盘游戏中对战,棋盘上有一行格子,长度为 n,每个格子上都有不同的分数。玩家 A 和玩家 B 轮流选择一个格子,他们都希望最终获得的总分数更高。规则是,每个玩家可以选择棋盘上的任何一端的格子,然后将该格子上的分数加入自己的总分。游戏结束后,获得最高总分的玩家获胜。

两位玩家都非常聪明,他们会采用最优策略来选择格子以最大化自己的得分。

输入

第一行为一个正整数 n

第二行为 n 个正整数

输出

输出一个整数,为两个玩家中的最高分

样例输入

4
10 40 60 20

样例输出

70

提示

当 A 先选的时候,在最外层的 10 和 20 之间选择 10 了以后,无论 B 怎么选择,60 必定落到 A 手里。所以 A 能拿到70分,B 再聪明也只能拿到60分。

思路:

这题没做出来,原本打算用双头队列,这样就方便判断两头出队的大小。后面看题解代码和卡哥的直播讲解,是用动态规划,随后理了一下思路,如下(摘自卡哥的题解):

/**
 * 思路:动态规划
 * 用F[l][r]表示先选的人能拿到的最高分
 * 用S[l][r]来表示后选的人能拿到的最高分
 * 对于先选者,有两种选法
 *     若先选者选A[0],则对于后面的1, ... ,n-1 数组,他就变成了后选者,此时能拿到的分为A[0]+S[1][n-1]
 *     若先选者选A[n-1],则对于前面的数组0,...,n-2,同样变为后选者,此时能拿到得分为A[n-1]+S[0][n-2];
 *     所以 F[0][n-1] = max(A[0]+S[1][n - 1],A[n - 1]+S[0][n - 2])
 * 对于后选者,他能能到的最高分是受先选者控制的,即他只能选到先选者留给他的最小值,将其转化为数学形式就是
 * S[l][r] = min(F[l + 1][r], F[l][r - 1]),因为先选者很聪明,肯定会把最低的分数留给他
*/

难点:看懂上面的思路之后,对于递推方程已经没问题了,但是在初始化的时候让我十分困惑,为什么是下面这样的:

for (int r = 0; r < n; ++r)
    {
        F[r][r] = A[r];
        S[r][r] = 0;
        for (int l = r - 1; l >= 0; l--)
        {
            F[l][r] = max(A[l] + S[l + 1][r], A[r] + S[l][r - 1]);
            S[l][r] = min(F[l + 1][r], F[l][r - 1]);
        }
    }

在对照着代码苦思冥想一阵后,决定打印一下样例的dp数组,发现是下面这样:

打印的F数组如下:
10 40 70 70
 0 40 60 60
 0  0 60 60
 0  0  0 20
 
打印的S数组如下:
0 10 40 60
0  0 40 60
0  0  0 20
0  0  0  0

使用到的元素都是对角线上半部分,F[r][r] = A[r]; 输入的数组组成了对角线,S[r][r] = 0; 由于F先于S拿棋子,所以在第一步S的代码都赋值的是0,F[0][n - 1], S[0][n - 1]最后第一行的最后一个数就是他们能够取到的最大数字和。

题解代码:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    int n;
    scanf("%d", &n);
    vector<int> A(n);
    vector<vector<int>> F(n, vector<int>(n)), S(n, vector<int>(n));
    for (int i = 0; i < n; ++i)
        scanf("%d", &A[i]);
    for (int r = 0; r < n; ++r)
    {
        F[r][r] = A[r];
        S[r][r] = 0;
        for (int l = r - 1; l >= 0; l--)
        {
            F[l][r] = max(A[l] + S[l + 1][r], A[r] + S[l][r - 1]);
            S[l][r] = min(F[l + 1][r], F[l][r - 1]);
        }
    }
    printf("%d\n", F[0][n - 1]);
    return 0;
}

如果您觉得本期博客写的不错,求个小小的赞,这对我是一种莫大的激励。 若是有其它意见,关于排版风格,关于代码注释情况,关于任何都欢迎在评论区提出来~