笔试面试题二(头条2019)

题目列表

1.变身程序员

题目描述

公司的程序员不够用了,决定把产品经理都转变为程序员以解决开发时间长的问题。

在给定的矩形网格中,每个单元格可以有以下三个值之一:

值 0 代表空单元格;
值 1 代表产品经理;
值 2 代表程序员;
每分钟,任何与程序员(在 4 个正方向上)相邻的产品经理都会变成程序员。

返回直到单元格中没有产品经理为止所必须经过的最小分钟数。

如果不可能,返回 −1。

以下是一个 4 分钟转变完成的示例:

2 1 1 2 2 1 2 2 2 2 2 2 2 2 2
1 1 0 -> 2 1 0 -> 2 2 0 -> 2 2 0 -> 2 2 0
0 1 1 0 1 1 0 1 1 0 2 1 0 2 2
输入格式
不固定多行(行数不超过 10),毎行是按照空格分割的数字(不固定,毎行数字个数不超过 10)。

其中每个数组项的取值仅为 0、1、2 三种。

读取时可以按行读取,直到读取到空行为止,再对读取的所有行做转换处理。

输出格式
如果能够将所有产品经理变成程序员,则输出最小的分钟数。

如果不能够将所有的产品经理变成程序员,则返回 −1。

输入样例1:
0 2
1 0
输出样例1:
-1
输入样例2:
1 2 1
1 1 0
0 1 1
输出样例2:
3
输入样例3:
1 2
2 1
1 2
0 1
0 1
1 1
输出样例3:
4

分析

本题考察多源BFS的最短步数问题,每分钟多个起点同时影响周围的点,使用多源BFS,最开始将所有2的位置加入到队列里,用一个二维数组cnt记录下每个1变成2所经历的分钟数。按行读取数据时可以用stringtream来提取数字,由于数字都不超过10,所以这里我直接使用循环分隔数据。

代码

#include 
#include 
#include 
#include 
using namespace std;
const int N = 12;
typedef pair PII;
int g[N][N];
int cnt[N][N];
int n,m;
queue q;
int dx[] = {0,0,1,-1},dy[]={1,-1,0,0};
bool check(int x,int y) {
    if(x < 0 || x >= n || y < 0 || y >= m)  return false;
    return g[x][y] == 1;
}
int main() {
    string s;
    int res = 0,ans = 0;
    while(getline(cin,s,'\n')) {
        for(int i = 0;i < s.size();i += 2) {
            g[n][i / 2] = s[i] - '0';
            if(s[i] == '1') res++;
        }
        n++;
        m = (s.size() + 1) / 2;
    }
    for(int i = 0;i < n;i++) {
        for(int j = 0;j < m;j++) {
            if(g[i][j] == 2) {
                q.push({i,j});
            }
        }
    }
    if(q.size())    ans = 1;
    while(res && q.size()) {
        PII u = q.front();
        q.pop();
        int i = u.first,j = u.second;
        for(int t = 0;t < 4;t++) {
            int nx = i + dx[t],ny = j + dy[t];
            if(check(nx,ny)) {
                res--;
                g[nx][ny] = 2;
                cnt[nx][ny] = cnt[i][j] + 1;
                ans = max(ans,cnt[nx][ny]);
                q.push({nx,ny});
            }
        }
    }
    if(!res) cout<

2.特征提取

题目描述

小明是一名算法工程师, 同时也是一名铲屎官。

某天,他突发奇想,想从猫咪的视频里挖掘一些猫咪的运动信息。

为了提取运动信息,他需要从视频的每一帧提取“猫咪特征”。

一个猫咪特征是一个两维的 vector

如果 x1=x2 并且 y1=y2, 那么这俩是同一个特征。

因此,如果猫咪特征连续一致,可以认为猫咪在运动。

也就是说,如果特征 在持续帧里出现,那么它将构成特征运动。

比如,特征 在第 2/3/4/7/8 帧出现,那么该特征将形成两个特征运动 2−3−4 和 7−8。

现在,给定每一帧的特征,特征的数量可能不一样。

小明期望能找到最长的特征运动。

输入格式
第一行包含一个正整数 M,代表视频的帧数。

接下来的 M 行,每行代表一帧,其中,第一个数字是该帧的特征个数,接下来的数字是在特征的取值;比如样例输入第三行里,2 代表该帧有两个猫咪特征,<1, 1> 和 <2, 2>。

输出格式
输出特征运动的长度作为一行。

数据范围
1≤M≤10000
输入特征总数和不超过 100000。
一帧的特征个数不超过 10000。
特征取值均为非负整数。

输入样例:
8
2 1 1 2 2
2 1 1 1 4
2 1 1 2 2
2 2 2 1 4
0
0
1 1 1
1 1 1
输出样例:
3
样例解释
特征 <1,1> 在连续的帧中连续出现 3 次,相比其他特征连续出现的次数大,所以输出 3。

分析

题目给出若干行数据,如果一个二元组在连续两帧里都出现了,那么此时二元组持续出现的次数就是2,求所有二元组中连续出现的最大次数。第一反应可能不知道考察的是什么,如果将本题的二元组变成单个元素,再将每行的特征数变成1个,问题就变成了在数组中找连续重复数字出现的次数最大是多少,比如1 2 2 2 3,2连续出现了三次。
本题也是一样,每行有多个特征值,我们对每个特征值逐个进行判断,设f[i]表示第i行某个特征值在第i行时已经连续出现的次数,则M帧中该特征连续出现的次数最大值就是max(f[i]),1<=i<=M。减而治之的考虑f[i]要如何计算。f[i] = f[i-1] + 1,翻译一下就是如果上一行该特征值出现了,则再第i行该特征值出现的次数就要加一;如果上一行该特征值没有出现,f[i-1] = 0,f[i] = 1。
下面用三种方法来实现本题。
方法一:

  • 记录下当前遍历到行的特征值cur以及从开始到第i行次数还可能增大的特征值total,cur和total都使用哈希表实现。
  • 遍历到第i行的特征值时,将特征值都插入到cur中,注意出现重复的特征值不要重复加入。
  • 遍历下total存储的特征值,如果在cur里面没有出现该特征值,就从total里面删掉该特征值,并尝试更新最优解;如果cur里面出现了该特征值,就将total里面该特征值的value加一。
  • 遍历下cur存储的特征值,如果total里面没有出现该特征值,将将该特征值加入到total里。
  • 遍历完所有行的特征值后,遍历下total,尝试更新下最优解。

该方法用total存储着每个特征值在遍历过程中的连续出现次数,虽然比较清晰,但是逻辑判断比较复杂,total里面的数量不会太大,因此效率也足以AC。
方法二:
分析下法一的麻烦之处,在于每次total和cur都要遍历下才能将total里的元素该增加的增加、该修改的修改、该删除的删除。这种麻烦是惯性思维引起的,我们很容易在脑海中维护一个total数组,然后逐行遍历特征值去更新它。事实上,total一开始的定义就是从开始到第i行次数还可能增大的特征值total,却忽略了到第i行之后可能增大出现次数的特征值恰好就是第i行的特征值,因此,我们只需要遍历每行时将特征值存入cur后,遍历下cur,如果特征值在total里面出现了,就改变下cur里面该特征值的value,最后将cur的值赋给total即可。
方法三:
既然前面的分析里,第i行特征值连续出现的次数f[i]仅与上一行是否出现该特征值有关,那么我们可以用pre记录下上一次特征值出现的行数,如果是上一行,将该特征值出现的次数继续++即可。这种方法是比较符合一开始动态规划的思想的,代码也最为简洁。
测评发现三种方法效率差不多,相对而言效率方法二最高,方法一最低。法一的多次遍历以及删除哈希表的操作比较耗时,效率最低是显然的;法三虽然代码简洁,但是每次遍历时pre都存储了下每个特征值上次出现的次数,到最后pre可能会是一个十万规模的哈希表,查找效率就变慢了;法二的高效在于遍历每一行的时候total和cur里面元素的数量最多只是当前行的特征值量,也就是一万,所以查找效率较高。

代码

方法一:

#include 
#include 
#include 
using namespace std;
typedef pair PII;
map total,cur;//unordered_map不能直接一pair为key,所以使用map
int main(){
    int n,m;
    PII x;
    scanf("%d",&m);
    int ans = 0;
    while(m--) {
        scanf("%d",&n);
        for(int i = 0;i < n;i++) {
            scanf("%d%d",&x.first,&x.second);
            cur[x] = 1;
        }
        //map的循环删除操作,使用基于范围的for循环虽然可以达到删除效果,但是循环的次数却会增加,引发问题,所以用该方式删除
        //比如map里有1,2两个key,循环时删掉了1,乱掉的指针会使得循环执行的次数可能不止两次
        for(auto it = total.begin();it != total.end();) {
            if(cur.count(it->first)) total[(it++)->first]++;
            else{
                ans = max(ans,total[it->first]);
                total.erase((it++)->first);
            }
        }
        for(auto &it : cur) {
            if(!total.count(it.first)) total[it.first] = 1;
        }
        cur.clear();
    }
    for(auto &it : total)   ans = max(ans,total[it.first]);
    printf("%d\n",ans);
    return 0;
}

方法二:

#include 
#include 
#include 
using namespace std;
typedef pair PII;
map total,cur;
int main(){
    int n,m;
    PII x;
    scanf("%d",&m);
    int ans = 0;
    while(m--) {
        scanf("%d",&n);
        for(int i = 0;i < n;i++) {
            scanf("%d%d",&x.first,&x.second);
            cur[x] = 1;
        }
        for(auto &it : cur) {
            PII k = it.first;
            if(total.count(k)) {
                cur[k] += total[k];
            }
            ans = max(ans,cur[k]);
        }
        total = cur;
        cur.clear();
    }
    printf("%d\n",ans);
    return 0;
}

方法三:

#include 
#include 
#include 
using namespace std;
typedef pair PII;
map total,pre;
int main(){
    int n,m;
    PII x;
    scanf("%d",&m);
    int ans = 0;
    for(int i = 1;i <= m;i++) {
        scanf("%d",&n);
        for(int j = 0;j < n;j++) {
            scanf("%d%d",&x.first,&x.second);
            if(pre[x] == i - 1) total[x]++;
            else    total[x] = 1;
            pre[x] = i;
            ans = max(ans,total[x]);
        }
    }
    printf("%d\n",ans);
    return 0;
}

3.机器人跳跃问题

题目描述

机器人正在玩一个古老的基于 DOS 的游戏。

游戏中有 N+1 座建筑——从 0 到 N 编号,从左到右排列。

编号为 0 的建筑高度为 0 个单位,编号为 i 的建筑高度为 H(i) 个单位。

起初,机器人在编号为 0 的建筑处。

每一步,它跳到下一个(右边)建筑。

假设机器人在第 k 个建筑,且它现在的能量值是 E,下一步它将跳到第 k+1 个建筑。

如果 H(k+1)>E,那么机器人就失去 H(k+1)−E 的能量值,否则它将得到 E−H(k+1) 的能量值。

游戏目标是到达第 N 个建筑,在这个过程中能量值不能为负数个单位。

现在的问题是机器人至少以多少能量值开始游戏,才可以保证成功完成游戏?

输入格式
第一行输入整数 N。

第二行是 N 个空格分隔的整数,H(1),H(2),…,H(N) 代表建筑物的高度。

输出格式
输出一个整数,表示所需的最少单位的初始能量值上取整后的结果。

数据范围
1≤N,H(i)≤105,

输入样例1:
5
3 4 3 2 4
输出样例1:
4
输入样例2:
3
4 4 4
输出样例2:
4
输入样例3:
3
1 6 4
输出样例3:
3

分析

本题考察二分,二分除了解决常见的单调数列的查找问题,也常常用于解在一定范围内,且可以判断解是否符合要求的这类最优解问题,尤其是在解决正常求解困难的题目上更加具有优势。
如果告诉你这题考察二分,百分之九十的人都能够解决,如果不提示使用二分,可能只有少数人能够联想到二分。下面说下怎么想到二分的,首先尝试正向求解,在第i个建筑上的能量,仅仅取决于这个建筑的高度以及在上一个建筑上的剩余能量,看似可以用DP求解,但是如果设f[i]为在第i个建筑上的能量值,根据f[i-1]值的不同,f[i]可能由两条路径转移而来,但是我们并不知道初始能量是多少。如果假设一个初始值呢?到了计算f[i]时发现两种状态转移过来得到的结果都是负数,此时我们尝试增加初始能量值,但是这样一来由于初始能量的变化,第i层以前的所有状态转移的路径都可能发生改变,不满足DP的状态的无后效性,所以此方法行不通。
试想下给多少初始值能够保证走到最后呢?答案是max(h[i]),因为只要能量不小于高度,一路上能量都会不断增加,肯定不会用完。这就告诉了我们最优解不超过max(h[i]),解所在的范围确定了,并且知道一个初始能量值,我们通过遍历可以判断这个能量能否走到最后,也就是知道了这个解是否符合要求,这就满足了二分求解的所有条件了。
最后本题还有一个坑,如果不自己模拟下用例很容易忽略,那就是判断能否走到最后时,一旦能量值超过了设定的范围十万,就可以立刻判断为true了,因为此后能量不会减少了。这个剪枝操作不仅是节省时间,更重要的是避免了能量溢出。当建筑的个数较多并且高度较矮时,能量会指数式上升,超过数据表示范围。比如有20个高度是1的建筑,我们二分50000时,第一个建筑上我们的能量就增加了49999,解决翻倍,第二个建筑能量又会增加99998,继续翻倍,到了第20个建筑能量就接近50000 * 220,也就是5 * 1010量级了,这还是只有二十个数的情况。

代码

#include 
using namespace std;
const int N = 100005;
int n,h[N];
bool check(int e) {
    for(int i = 0;i < n;i++) {
        if(h[i] > e) {
            e -= h[i] - e;
            if(e < 0)   return false;
        }
        else{
            e += e - h[i];
            if(e >= 100000) return true;
        }
    }
    return true;
}
int main(){
    scanf("%d",&n);
    for(int i = 0;i < n;i++) scanf("%d",&h[i]);
    int l = 1,r = 100000;
    while(l < r) {
        int mid = l + r >> 1;
        if(check(mid))  r = mid;
        else    l = mid + 1;
    }
    printf("%d\n",l);
    return 0;
}

4.毕业旅行问题

题目描述

小明目前在做一份毕业旅行的规划。

打算从北京出发,分别去若干个城市,然后再回到北京,每个城市之间均乘坐高铁,且每个城市只去一次。

由于经费有限,希望能够通过合理的路线安排尽可能的省些路上的花销。

给定一组城市和每对城市之间的火车票的价钱,找到每个城市只访问一次并返回起点的最小车费花销。

注意:北京为 1 号城市。

输入格式
城市个数 n。

城市间的车票价钱 n 行 n 列的矩阵 m[n] [n]。

输出格式
输出一个整数,表示最小车费花销。

数据范围
1 车票价格均不超过 1000 元。

输入样例:
4
0 2 6 5
2 0 4 4
6 4 0 2
5 4 2 0
输出样例:
13
说明
共 4 个城市,城市 1 和城市 1 的车费为 0,城市 1 和城市 2 之间的车费为 2,城市 1 和城市 3 之间的车费为 6,城市 1 和城市 4 之间的车费为 5,以此类推。

假设任意两个城市之间均有单程票可买,且价格均在 1000 元以内,无需考虑极端情况。

分析

本题考察最短Hamilton问题,相关的题解见AcWing 91 最短Hamilton路径。
求出起点经过所有节点一次后的最小距离后,再加上最小距离所在的终点到起点的距离,就是本题的答案了。由于每个城市只能经过一次,所以不用考虑到终点后是否存在回到起点更短的路径。具体的题解见上面的链接,下面简要的描述下解题过程。

设f[i][j]表示从起点到j节点且经过的节点集合是i的最小距离。那么我们可以通过枚举经过路径上倒数第二个节点来求解最小距离,即
f[i][j] = min(f[i][j],f[i_k][k] + d[k][j]), f[i_k][k]表示起点到倒数第二个节点k的最小距离,i_k表示经过的节点集合i去掉k后节点的集合。
我们可以通过枚举所有的路径状态,从1一直枚举到(1 << n) - 1。比如n = 5时,状态11011就表示经过了1,2,4,5四个节点的节点集合,
由于从起点出发到终点是2,4,5的点都可能产生该节点集合,所有需要进一步的枚举终点是哪个,枚举完终点后才能枚举倒数第二个节点来更新最小值。

代码

#include 
#include 
#include 
using namespace std;
const int N = 22, M = 1 << 20;
int f[M][N],d[N][N];
int main() {
    int n;
    cin>>n;
    for(int i = 0;i < n;i++)
        for(int j = 0;j < n;j++)    cin>>d[i][j];
    memset(f,0x3f,sizeof f);
    f[1][0] = 0;
    for(int i = 1;i < 1 << n;i++) {
        if(!(i & 1)) continue;
        for(int j = 0;j < n;j++) {
            if(i >> j & 1) {
                int t = i - (1 << j);
                for(int k = 0;k < n;k++){
                    if(t >> k & 1) {
                        f[i][j] = min(f[i][j],f[t][k] + d[k][j]);
                    }
                }
            }
        }
    }
    int ans = 0x3f3f3f3f;
    for(int i = 0;i < n;i++)    ans = min(ans,f[(1 << n) - 1][i] + d[i][0]);
    cout<

5.过河

题目描述

有 n 个人要过河,但是河边只有一艘船;

船每次最多坐三个人,每个人单独坐船过河的时间为 a[i];

两个人或者三个人一起坐船时,过河时间为他们所有人中的最长过河时间;

为了安全起见,要求每次至少有两个人才能过河。

问最短需要多少时间,才能把所有人送过河。

输入格式
第一行是整数 T,表示测试样例个数。

每个测试样例的第一行是一个正整数 n,表示参加过河的人数。

第二行是 n 个正整数 a[i],表示 n 个人单独过河的时间;

输出格式
每个测试样例,输出一个最少的过河时间。

每个结果占一行。

数据范围
2≤n<100000,
0 输入样例:
2
2
1 2
4
1 1 1 1
输出样例:
2
3

分析

本题考察动态规划+贪心,如果没有做过类似问题,想到求解方法还是很难的。题目意思是只有一艘船,要把所有人送到对岸,且船上至少两人,至多三人,这就意味着三个人坐船过河后两个人要返回接其他人,每次最多让一人到达对岸。
由于每个人坐船过河的时间不同,我最开始的想法就是先对所有人坐船时间做个排序,选两个时间最小的做船夫,逐个把其他人送过去,因为耗时包括过河时间和回来的时间,既然耗时较大的人过河时间不可避免,那么让回来的时间尽量小即可,也就是选两个时间最小的船夫。用这种贪心的思想实现了代码,只过了很少的用例。然后发现,这种办法只是最优情况的一种,只考虑到了每次送一人的情况,事实上,开始想到一个时间少的人带两个时间大的人过河后,还得回来其中两个,增加回来的耗时不说还只送了一个人到对岸,得不偿失,就认为开始的贪心做法是最优的了。正确的贪心做法可能只有做过过河的相关题型后才能够想到,原先贪心算法的瑕疵在于尽管每次过河三人就得回来两人,但是回来的两人未必就要是这次过河当中的人,也可以从此前已经过河的人当中选耗时少的做船夫,比如河对岸有若干个耗时1的人,我们用1个耗时1的人送两个耗时100的人过河,再派两个耗时1的人将船开回来,这样比两个耗时100的分两次过河就更加节省时间了。
用全集划分思想划分本题的状态,可以分为:每次过河送1个耗时长的,送2个耗时长的,送3个耗时长的三种情况,出于贪心考虑,我们容易得出,送一个时间长的过河这种情况只需要我们选两个耗时最短的人做船夫,送过去后再回来即可;送两个耗时长的人过河后,除了之前送他们的一人,还需要一人才能将船开回去,显然我们不能让耗时长的再回去,在河对岸的人群中选谁回去比较合适呢?答案是选谁都不合适,回去的船夫要求耗时越短越好,这样才能做到回去耗时最少。换种思路想,出于最优考虑,耗时最短的几人,还是最后再过河比较好,充当船夫比较划算,所以送两人的情况我们可以先送1个耗时短的过河备用,然后船回来后再送两个耗时长的过河,这样一来对岸便有了两个耗时短的了,足以将船开回来,并且好处是这种状态只是送了两个耗时长的过河,其他人位置未变,便于后面我们状态的转移;第三种情况是送三个人过河,有了送两人过河的经验,我们可以想到,我们先用两个来回,送两个耗时短的人过河备用,然后三个耗时长的乘船过河,两个耗时短的开船回来。可以发现这种求解的步骤与汉诺塔问题颇为相似。
贪心的思想讲完了,下面用DP解决本题,我们先对过河时间数组a进行排序,使得a[0]最小,a[n - 1]最大。状态表示:f[i]表示从a[n-1]一直到a[i]这么多人过河的最短时间。这么表示状态也是出于过河时间长的先过河这种贪心思想。我们按照第i个人过河时的方案可以将状态划分为:i由两个短时间的送过河,i和i + 1由一个短时间的送过河,i,i + 1,i + 2一起过河,i所有的过河方案必然是这三种方案之一。可以进一步得出

  • 一个人过河:f[i] = f[i+1] + a[i] + a[1],也就是i到n-1人过河时间等于i + 1到n-1的过河时间,加上两个耗时短的送第i个人的过河时间a[1],再加上两个船夫回来的时间a[1]。
  • 两个人过河:f[i] = f[i + 2] + a[2] + a[1] + a[i + 1] + a[2],也就是i + 2到n-1的过河时间加上a[0]、a[1]把a[2]运过去的时间a[2],加上a[0]、a[1]回来的时间a[1],加上a[1]和a[i]、a[i+1]过河的时间a[i+1],最后再加上a[1]、a[2]回来的时间。
  • 三个人过河:f[i] = f[i+3] + a[2] + a[1] + a[3] + a[1] + a[i + 2] + a[3],也就是i + 3到n-1的过河时间加上a[0]、a[1]带a[2]过去的时间a[2],加上a[0]、a[1]回来的时间a[1],加上a[0]、a[1]带a[3]过去的时间a[3],加上a[0]、a[1]回来的时间a[1];加上a[i]、a[i+1]、a[i+2]过去的时间a[i+2],最后再加上a[2]、a[3]回来的时间a[3]。
    看起来分类的情况相当复杂,实际上比如三个人过河,只要记住先两个来回送两个时间短的过河再三个人过河即可,f[i]的时间是i到n-1都过河了,其他人没过河,并且船已经开回来的场景。对于多个船夫,每次选哪个过河的问题,分析后可以知道选谁对最后的耗时没有影响。
    PS:本题数据范围会超int,使用long long存储dp数组,并且使用printf输出结果时记得使用%lld控制格式。(开始疏忽用%d输出导致答案被截断,错了几个用例 )。

代码

#include 
#include 
using namespace std;
typedef long long ll;
const int N = 100005;
int a[N];
ll f[N];
int main(){
    int T,n;
    scanf("%d",&T);
    while(T--) {
        scanf("%d",&n);
        for(int i = 0;i < n;i++)    scanf("%d",&a[i]);
        sort(a, a + n);
        if(n == 2)  printf("%d\n",a[1]);
        else{
            f[n] = 0;
            for(int i = n - 1;i >= 3;i--) {
                //a[0],a[1]运a[i]过去,a[0],a[1]回来
                f[i] = f[i + 1] + a[1] + a[i];//运一个人过河
                //a[0],a[1]把a[2]运过去,然后a[0],a[1]回来,a[1]过去带走a[i]、a[i+1],a[1],a[2]回来
                if(i <= n - 2)    f[i] = min(f[i],f[i + 2] + a[2] + a[1] + a[i + 1] + a[2]);//运两个人过河
                //a[0],a[1]带a[2]过去,a[0],a[1]回来,a[0],a[1]带a[3]过去,a[0],a[1]回来;a[i],a[i+1],a[i+2]过去,a[2],a[3]回来
                if(i <= n - 3)  f[i] = min(f[i],f[i + 3] + a[2] + a[3] + 2 * a[1] + a[i + 2] + a[3]);//运三人过河
            }
            printf("%lld\n",f[3] + a[2]);
        }
    }
    return 0;
}

你可能感兴趣的:(其它,笔试题)