今日头条 2018 AI Camp 5 月 26 日在线笔试编程题第一道——最佳路径

题目

给定一个 n*m 的矩阵 A ,矩阵中每一个元素为一个十六进制数。寻找一条从左上角都右下角的路径,每次只能向右或者向下移动,
使得路径上所有数字之积在 16 进制下的后缀 0 最少。

输入描述:

第一行:n, m (2 <= n,m <= 1000)
接下来 n 行,每行 m 个 16 进制整数 0 <= a i j <= 10 9

输出描述:

第一行:最少后缀 0 的个数(十进制)
第二行:路径方案,从左上角开始,”>” 代表向右移动,”V” 代表向下移动。
如果有多种方案,输出字典序最小的方案(“>” 的字典序小于 “V”)。

示例:
  • 输入

    3 3
    3 2 8
    c 8 8
    2 a f

  • 输出

    1
    ‘>>VV’(此处输出实际上没有引号)

  • 说明
    从左上角到右下角的所有路径中, 0x3 * 0x2 * 0x8 * 0x8 * 0xf = 0x1680 后缀 0 最少为 1, 且路径 “>>VV” 的字典序最小。

思路

  • 首先需要设计一个函数来判断十六进制数字末尾零的个数。可以分为两种情况,小于 16 的数只有 0 末尾有一个零;大于等于 16 的数如果最后一位是 0,则肯定可以被 16 整除,若末尾是 0,我们对这个十六进制数向右移一位,也即除以 16,再看倒数第二位是否是零,这样一直往前判断直到某一位非零为止。

  • 路径判断则用动态规划来实现。定义两个变量,第一个变量保存从左上角到每一个位置处的乘积,第二个变量保存是怎样从前一步到当前位置的,只有向右或者向下两种情况,用枚举表示。第一行只能向右走,第一列只能向下走,这是初始化情况。然后从第二行第二列开始进行判断,每一步比较从左边来的乘积和从上边来的乘积末尾含有零的个数,若二者不相等,则保存乘积和移动方向到相应变量中。若向下和向右二者相等,则需要分别向上和向左回溯到左上角倒序求出移动方向,然后从头开始比较,选择字典序小的路径作为最终的移动方向。

样例展示 (从左到右分别是原始数字、十六进制乘积和移动方向)

3 o 2 (6) (>) 8 (30) (>)
c (24) (V) 8 (30) (V) 8 (180) (V)
2 (48) (V) a (1E0) (V) f (1680) (V)

第二行第二个位置,从左边来是 24×8 = 120,末尾有 1 个 0;从上边来是 6×8 = 30,也有 1 个 0。
从左边来路径是 V>,从右边来路径是 >V,由于 > 字典序小于 V,因此最终移动方向为 >V。

代码实现

#include <iostream>
#include <vector>
using namespace std;

int find_zero_num(int number);
int min_dictionary(int *direction, int i, int j, int col);
void find_route(int *direction, vector<int> &route, int m, int n, int col);

enum {RIGHT = 0, DOWN = 1}; //向下走为 1 ,向右走为 0

int main()
{
   int n = 0, m = 0;
   cin >> n >> m;

   int data[n][m];           
   //保存矩阵中的数据
   long int product[n][m];  
   //保存从左上角到位置(i, j)处的乘积
   int direction[n][m] = {0};
   //保存位置(i, j)处的乘积是怎么得到的,1为从前一位置往下,0为从前一位置往右

   vector<int> route; //倒序保存路径

   int i = 0, j = 0;

   for(i = 0; i < n ; i++)
   {
       for(j = 0; j < m; j++)
       {
           cin >> hex >> data[i][j];
       }
   }

   // 初始化第一列的乘积作为边界值
   product[0][0] = data[0][0];
   for(i = 1; i < n; i++)
   {
       product[i][0] = product[i-1][0] * data[i][0];
       direction[i][0] = DOWN;
   }

   // 初始化第一行的乘积作为边界值
   for(j = 1; j < m; j++)
   {
       product[0][j] = product[0][j-1] * data[0][j];
       direction[0][j] = RIGHT;
   }

   long int down = 0;
   long int right = 0;
   int flag = 0;

   for(i = 1; i < n; i++)
   {
       for (j = 1; j < m; j++)
       {
           down = product[i-1][j] * data[i][j]; //往下走的乘积
           right = product[i][j-1] * data[i][j]; //往右走的乘积

           if (find_zero_num(down) < find_zero_num(right))
           {
               flag = 1;
           }
           else if (find_zero_num(down) > find_zero_num(right))
           {
               flag = RIGHT;
           }
           else            //若向下和向右一样,则优先取字典序小的
           {
               if(min_dictionary(*direction, i, j, m))
               {
                   flag = DOWN;
               }
               else
               {
                   flag = RIGHT;
               }
           }

           if(!flag)
           {
               product[i][j] = right;
               direction[i][j] = RIGHT;
           }
           else
           {
               product[i][j] = down;
               direction[i][j] = DOWN;
           }
       }
   }

   cout << find_zero_num(product[n-1][m-1]) << endl;

   find_route(*direction, route, n-1, m-1, m);

   for(i = int(route.size()-1); i >= 0; i--)
   {
       if (route[i])
       {
           cout << 'V';
       }
       else
       {
           cout << '>';
       }
   }

   return 0;
}

// find the zero number of a hex data
int find_zero_num(int number)
{
   int sum = 0;
   if(number == 0)
   {
       sum = 1;
   }
   while (number % 16 == 0 && number >= 16)
   {
       sum++;
       number = number / 16;
   }
   return sum;
}

int min_dictionary(int *direction, int i, int j, int col)
{
   vector<int> route_down;
   vector<int> route_right;


   int m = i - 1;
   int n = j;
   find_route(direction, route_down, m, n, col); //向上回溯路线

   m = i;
   n = j - 1;
   find_route(direction, route_right, m, n, col); //向左回溯路线


   int length = int(route_right.size());

   for (i = length - 1; i >= 0; i--) //从第一个不相等的位置处开始判断字典序
   {
       if (route_right[i] < route_down[i])
       {
           return 0; //向右字典序小
       }
       if (route_right[i] > route_down[i])
       {
           return 1; //向下字典序小
       }
   }

   return -1;
}

// 从位置 (m, n) 处回溯路线,倒序保存在向量中
void find_route(int *direction, vector<int> &route, int m, int n, int col)
{
   while(1)
   {
       // 此位置乘积由上一位置向下移动得来,行数减一继续寻找
       if(direction[m * col + n] == 1)
       {
           m = m - 1;
           route.push_back(1);
       }
       // 此位置乘积由上一位置向右移动得来,列数减一继续寻找
       else
       {
           n = n - 1;
           route.push_back(0);
       }

       // 寻找至左上角,结束
       if (m == 0 && n == 0)
       {
           break;
       }
   }
}

个人见解,如有错误,欢迎指正与交流!

获取更多精彩,请关注「seniusen」!

全部评论

相关推荐

上周组里招人,我面了六个候选人,回来跟同事吃饭的时候聊起一个让我挺感慨的现象。前三个候选人,算法题写得都不错。第一道二分查找,五分钟之内给出解法,边界条件也处理得干净。第二道动态规划,状态转移方程写对了,空间复杂度也优化了一版。我翻他们的简历,力扣刷题量都在300以上。后三个呢,就有点参差不齐了。有的边界条件没处理好,有的直接说这道题没刷过能不能换个思路讲讲。其中有一个女生,我印象特别深——她拿到题之后没有马上写,而是先问我:“面试官,我能先跟你确认一下我对题目的理解吗?”然后她把自己的思路讲了一遍,虽然最后代码写得不是最优解,但整个沟通过程非常顺畅。这个女生的代码不是最优的,但当我问她“如果这里是线上环境,你会怎么设计’的时候,她给我讲了一套完整的方案——异常怎么处理、日志怎么打、怎么平滑发布。她对这是之前在实习的时候踩过的坑。”我在想LeetCode到底在筛选什么?我自己的经历可能有点代表性。我当年校招的时候,也是刷了三百多道题才敢去面试。那时候大家都刷,你不刷就过不了笔试关。后来工作了,前三年基本没再打开过力扣。真正干活的时候,没人让你写反转链表,也没人让你手撕红黑树。更多的是:这个接口为什么慢了、那个服务为什么OOM了、线上数据对不上了得排查一下。所以后来我当面试官,慢慢调整了自己的评判标准。算法题我还会出,但目的变了。我出算法题,不是想看你能不能背出最优解。而是想看你拿到一个陌生问题的时候,是怎么思考的。你会先理清题意吗?你会主动问边界条件吗?你想不出来的时候会怎么办?你写出来的代码,变量命名乱不乱、结构清不清楚?这些才是工作中真正用得到的能力。LeetCode是一个工具,不是目的。它帮你熟悉数据结构和常见算法思路,这没问题。但如果你刷了三百道题,却说不清楚自己的项目解决了什么问题、遇到了什么困难、你是怎么解决的,那这三百道题可能真的白刷了。所以还要不要刷LeetCode?要刷,但别只刷题。刷题的时候,多问自己几个为什么:为什么用这个数据结构?为什么这个解法比那个好?如果换个条件,解法还成立吗?把刷题当成锻炼思维的方式,而不是背答案的任务。毕竟面试官想看到的,从来不是一台背题机器,而是一个能解决问题的人。
牛客51274894...:意思是光刷力扣还不够卷
AI时代还有必要刷lee...
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务