美文网首页
阿里巴巴2020暑期实习笔试题

阿里巴巴2020暑期实习笔试题

作者: 牛奶芝麻 | 来源:发表于2020-08-25 13:51 被阅读0次

    第一场(2020/03/20):

    题目一:

    有一叠扑克牌,每张牌介于1和10之间。有四种出牌方法:

    • 单出一张
    • 出两张相同的牌(对子)
    • 出五张顺子(如12345)
    • 出三连对子(如112233)

    给10个数,表示1-10每种牌有几张,问最少要多少次能出完。每种牌最多有四张。

    解题思路:

    DFS 回溯法,先判断组成三连对和组成顺子需要的次数,递归深度 k 就是次数。对于对子和单张的可以直接通过枚举数需要打多少次。可以在组成三连对和顺子的时候增加剪枝操作加快运算:如果构不成三连对或者顺子,则不用进行回溯。

    时间复杂度大概为 O(4^10)。

    C++ 实现:
    #include<bits/stdc++.h>
    using namespace std;
    int a[15],ans=123456789;
    
    void dfs(int k)  // k表示次数 
    {
        //检查是否有三连对子
        for(int i=1;i<=8;i++) {
            bool flag = true;
            for(int j=i;j<i+3;j++)
                if(a[j] < 2) {  // 不能构成三连对
                    flag = false;  // 剪枝 
                    break;
                }
            if(flag == true) {  // 可以构成三连对
                for(int j=i;j<i+3;j++)
                    a[j] -= 2;
                dfs(k+1);
                for(int j=i;j<i+3;j++)  // 回溯一步
                    a[j] += 2;
            }
        } 
        //检查是否有顺子
        for(int i=1;i<=6;i++) {
            bool flag = true;
            for(int j=i;j<i+5;j++) 
                if(a[j] < 1) {  // 不能构成顺子
                    flag = false;  //  剪枝 
                    break;
                }
            if(flag == true) {   // 可以构成顺子
                for(int j=i;j<i+5;j++)
                    a[j] -= 1;
                dfs(k+1);
                for(int j=i;j<i+5;j++)   // 回溯一步
                    a[j] += 1;
            } 
        }
        //没有三连对和顺子,直接统计答案
        int cnt = 0;
        for(int i=1;i<=10;i++) {
            if(a[i]==4) cnt += 2;   // 直接构成两个一对
            else if(a[i]==3) cnt+=2;  // 一对,一单张
            else if(a[i]==2) cnt+=1;  // 一对
            else if(a[i]==1) cnt+=1;  // 一单张
        }
        ans = min(ans, cnt+k);   // 更新答案
    } 
    
    int main()
    {
        for(int i=1;i<=10;i++) cin >> a[i];
        dfs(0);
        cout << ans;
        return 0;
    }
    

    题目二:

    小明在学旋律,每段旋律都可以用字符串来表示,并且旋律的每个字符的ASCALL码递增的。比如以下4段旋律 :
    aaa
    bcd
    bcdef
    zzz
    现在就是要求,小明能够吧这些旋律拼接起来组成最长的旋律。
    比如上述例子输出 11 最长的旋律为 aaabcdefzzz

    解题思路:

    实际上,这道题和之前做过的 Leetcode 【DP】139. Word Break 很类似。

    排序 + 动态规划(DP):

    • 首先按照字符串最后一个字母,由小到大排序,如果最后一个相同,按第一个由小到大字母排序。
    • 然后定义 dp 数组,对于每个字符串 str,dp[i] 表示从字母 'a' 到字母 str[str.size()-1] 为结尾的最长上升字符串的长度。
    • 枚举输入的字符串 s,s 的最后一个字母 end = s[s.size()-1]。从字符串首字母 s[0] 到字母 'a' 倒着循环(假设循环遍历为 i),使用状态转移方程 dp[end] = max(dp[end], dp[i] + s.size()) 来更新 dp[end] 的最大值。
    • 最后,dp['a'] ~ dp['z'] 中的最大值就是答案。

    比如上述例子:
    (0)按照排序规则排完后为 aaa、bcd、bcdef、zzz,建立一个 dp[515],初始化全为 0。然后计算:
    (1)dp['a'] = max(dp['a'], dp['a'] (首字母) + 3) = 3;
    (2)dp['d'] = max(dp['d'], dp['b'] (首字母) + 3) = 3;dp['d'] = max(dp['d'], dp['a'] + 3) = 3 + 3 = 6;
    (3)dp['f'] = max(dp['f'], dp['b'] (首字母) + 5) = 5;dp['f'] = max(dp['f'], dp['a'] + 5) = 3 + 5 = 8;
    (4)dp['z'] = max(dp['z'], dp['z'] (首字母) + 3) = 3;dp['z'] = max(dp['z'], dp['y'] + 3) = 3;dp['z'] = max(dp['z'], dp['x'] + 3) = 3;...... ;dp['z'] = max(dp['z'], dp['f'] + 3) = 8 + 3 = 11;...... ;dp['z'] = max(dp['z'], dp['d'] + 3) = max(11, 6 + 3) = 11;...... ;dp['z'] = max(dp['z'], dp['a'] + 3) = max(11, 3 + 3) = 11.
    (5)最后,找出 dp['a'] ~ dp['z'] 中的最大值就是答案。

    简单地说,比如一个字符串是 defg,我们就看一下字母 d 之前的能不能连接到以 g 为结尾的字符串后面,因此要循环 d、c、b、a,来更新 dp[g] = max(dp[g], dp[d(或者c\b\a)]+4)。

    现在来解释两个问题:

    • 为什么排序规则是上面那样的?
      首先,按照末尾字符串先从小到大排序这个毋庸置疑,因为比如 abc、fgh,要计算 dp[h],需要用到 dp[f] 到 dp[a] 这些之前的结果;
      然后,如果末尾字符串相同,为什么还要按照首字母排序呢?因为比如 ab、bb,要计算字符串 bb 的 dp[b],需要用到 dp[b] 和 dp[a] 的结果,如果不按照首字母排序,那么可能 bb 会出现在 ab 前面。求解 bb 的时候 dp[a] 是 0,因此就会产生错误答案 2,而不是 4。

    • 为什么在更新 dp[i] 时要从 s[0] 到 'a' 倒着循环?
      这是因为比如 s = "bb",如果倒着循环可以得到正确答案 2,但是正着循环,从 'a' 到 s[0] = 'b',更新 dp['b'] 的时候,按照上面的状态转移方程,"bb" 会重复计算一次,得到 dp['b'] = 4 的错误答案。

    空间复杂度为 O(26),时间复杂度为 O(26*n)。

    C++ 实现:
    #include<bits/stdc++.h>
    using namespace std;
    struct node {
        string s;
        int len;
    };
    node xuan[1000005];
    bool cmp(node a, node b) {  // 排序规则
        if(a.s[a.len-1] < b.s[b.len-1]) return true;
        else if(a.s[a.len-1] > b.s[b.len-1]) return false;
        else {
            return a.s[0] < b.s[0];
        }
    }
    int f[500];  // f[i]表示从字符'a'到字符串最后一个字符的最大长度 
    int main()
    {
        int n;
        cin >> n;
        for(int i=1;i<=n;i++) {
            cin >> xuan[i].s;
            xuan[i].len = xuan[i].s.size();
        }
        sort(xuan+1,xuan+n+1,cmp);
        for(int i=1;i<=n;i++) {
            int start = xuan[i].s[0];//第一位字符的ASCLL值 
            int end = xuan[i].s[xuan[i].len-1];//最后一位字符的ASCLL值 
            for(char c=start; c>='a';c--) //必须从start开始倒着循环到'a' 
                f[end] = max(f[end], f[c]+xuan[i].len);
        }
        int ans = 0;
        for(char c='a';c<='z';c++)
            if(f[c]>ans) ans = f[c];
        cout << ans;
        return 0;
    }
    

    第二场(2020/03/23):

    题目一:

    N个人,任意选k个,再从k个里任选1个当队长,求总组合数。

    解题思路:

    总数 S = 0*C(N,0) + ... + i*C(N,i) + N*C(N,N)
    倒着加:S + S = N*(C(N,0) + ... + C(N,N) = N*2^(N)
    所以 S = N*2^(N-1)

    接下来只需要考虑计算 2^(N-1) 的快速计算方法就好了(快速幂算法)

    时间复杂度O(logN),可以AC

    C++ 实现:
    #include<bits/stdc++.h>
    using namespace std;
    const int mod=1e9+7;
    typedef long long ll;
    
    ll ksm(ll a, ll b)  // 快速幂算法
    {
        ll ans=1, base=a;
        while(b) {
            if(b&1) ans=ans*base%mod;
            base=base*base%mod;
            b>>=1;
        }
        return ans%mod;
    }
    
    int main() {
        ll n;
        cin>>n;
        ll ans=n*ksm(2,n-1)%mod;
        cout<<ans<<endl;
        return 0;
    }
    

    题目二:

    一个地图 n*m,包含1个起点,1个终点,其他点包括可达点和不可达点。每一次可以:上下左右移动,或使用1点能量从(i,j) 移动到(n-1-i, m-1-j),最多可以使用5点能量。求从起点到达终点的最短路径长度。

    解题思路:

    类似于广度优先搜索的(使用队列),只不过还要考虑到达某个坐标有能量时,也可以进行跳跃,因此也可以入队列。

    因为队列入队时,肯定越到后面步数需要越多,所以其实第一次访问到终点坐标就可以跳出了,即就是最短路径。

    空间复杂度 O(m*n), 时间复杂度 O(m*n)。

    C++ 实现:
    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=1e5+5;
    typedef long long ll;
    struct node {  // 某个节点信息
        int x,y;
        int stp;  // 到达这个节点时所走的步数
        int cnt;  // 到达这个节点时还有多少能量
    };
    int dir[4][2]={{0,1},{0,-1},{1,0},{-1,0}};
    int sx,sy;   // 起点坐标
    int n,m;
    int ex,ey;  // 终点坐标
    bool check(int x,int y){
        if(x>=1&&x<=n&&y>=1&&y<=m){
            return true;
        }
        else{
            return false;
        }
    }
    char Map[505][505];  // 地图
    int vis[505][505];  // 标记某个点是否走过
    
    int  BFS() {
        queue<node>q;
        node st;
        st.x=sx;
        st.y=sy;
        st.stp=0;
        st.cnt=5;
        vis[sx][sy]=1;
        q.push(st);  // 起点入队列
        while(!q.empty()){
            node now =q.front();
            if(now.x==ex&&now.y==ey)return now.stp;  // 最短路径即是第一次到达的步数
            q.pop();
            node nxt;
            for(int i=0;i<4;i++) {   // 上下左右搜索
                nxt.x=now.x+dir[i][0];
                nxt.y=now.y+dir[i][1];
                nxt.stp=now.stp+1;
                nxt.cnt=now.cnt;
                if(check(nxt.x,nxt.y)&&vis[nxt.x][nxt.y]==0&&(Map[nxt.x][nxt.y]=='.'||Map[nxt.x][nxt.y]=='E')){  // '.' 为可达
                   vis[nxt.x][nxt.y]=1;
                   q.push(nxt);
                }
            }
            if(now.cnt>=1){   // 如果到达 now 这个节点还有能量
                nxt.x=n+1-now.x;
                nxt.y=m+1-now.y;
                nxt.stp=now.stp+1;
                nxt.cnt=now.cnt-1;
                if(check(nxt.x,nxt.y)&&vis[nxt.x][nxt.y]==0&&(Map[nxt.x][nxt.y]=='.'||Map[nxt.x][nxt.y]=='E')){  // '.' 为可达
                   vis[nxt.x][nxt.y]=1;
                   q.push(nxt);
                }
            }
        }
        return -1;   // 队列为空,不可达
    }
    
    int main(){
        cin>>n>>m;
        for(int i=1;i<=n;i++){
            scanf("%s",Map[i]+1);
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                if(Map[i][j]=='S'){  // 起点
                    sx=i;sy=j;
                }
                if(Map[i][j]=='E'){  // 终点
                    ex=i;ey=j;
                }
            }
        }
        cout<<BFS()<<endl;
        return 0;
    }
    

    第三场(2020/03/25):

    题目一:

    给定一个 3 行 n 列的矩阵 a (n<=10^5),从每一列选择一个数 bi 组成一个数组,然后要求使这个数组前一项减后一项的绝对值之和最小。即求
    ∑ |b(i) - b(i+1)| (i=1,2,...,n-1)

    输入示例:

    5
    5  9  5  4  4
    4  7  4  10 3
    2  10 9  2  3
    这里可以选择 [5 7 5 4 4],所以输出等于 |7-5|+|5-7|+|4-5|+|4-4|=5。所以输出就是5。
    
    解题思路:

    动态规划。这道题和 Leetcode 【DP】64. Minimum Path Sum 很类似,使用 f[i][j] 存储到达点 (i, j) 的最小绝对值之和,最后 min({f[1][n],f[2][n], f[3][n]}) 就是最终的答案。状态转移方程如下:
    f[i][j] = min(f[k][j-1] + abs(a[k][j-1] - a[i][j])), k = 1, 2, 3
    边界情况:f[1][1] = f[2][1] = f[3][1] = 0 (初始绝对值累加和为0)

    时间复杂度为 O(9*n),空间复杂度为 O(3*n)。

    C++ 实现:
    #include<iostream>
    #include<algorithm> 
    #include<cstring>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    ll a[4][100005], f[4][100005];
    
    int main()
    {
        int n;
        cin>>n;
        for(int i=1; i<=3; i++) {
            for(int j=1; j<=n; j++) {
                cin>>a[i][j];
            }
        }
        memset(f, 0x7f, sizeof(f));  // 相当于 0x7f7f7f7f (int下)
        f[1][1] = f[2][1] = f[3][1] = 0;
        for(int j=2; j<=n; j++) {  // 先要遍历列
            for(int i=1; i<=3; i++) {  // 再遍历行
                for(int k=1; k<=3; k++) {
                    f[i][j] = min(f[i][j], f[k][j-1] + abs(a[i][j] - a[k][j-1]));
                }
            }
        }
        cout<<min(f[1][n], min(f[2][n], f[3][n]));
    }
    
    优化:

    因为 f[i][j] = min(f[k][j-1] + abs(a[k][j-1] - a[i][j])), k = 1, 2, 3,则发现第 j 列只依赖于 j-1 列,因此没必要开辟一个 f[i][j] 大小的二维数组,只需要开辟两个一维数组:f[4] 和 pre[4],其中 f[i] 记录当前列每一行绝对值累加和,pre[i] 记录上一列每一行绝对值累加和。 因此状态转移方程为:
    f[i] = min(pre[k] + abs(a[k][j-1] - a[i][j])), k = 1, 2, 3
    边界情况:pre[1] = pre[2][1] = pre[3][1] = 0 (初始绝对值累加和为0)
    需要注意的是,每次求 f 数组的每一个 f[i] 时,都要先初始化为最大值,并且每次计算完 f 数组后,都要将 f 数组拷贝给 pre 数组。

    时间复杂度为 O(12*n),空间复杂度为 O(1) (只有两个大小为 4 的数组)。

    空间优化的 C++ 代码实现:
    #include<iostream>
    #include<algorithm> 
    #include<cstring>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    // f数组记录当前列每一行绝对值累加和,pre数组记录上一列每一行绝对值累加和 
    ll a[4][100005], f[4], pre[4];
    
    int main()
    {
        int n;
        cin>>n;
        for(int i=1; i<=3; i++) {
            for(int j=1; j<=n; j++) {
                cin>>a[i][j];
            }
        }
        pre[1] = pre[2] = pre[3] = 0;
        for(int j=2; j<=n; j++) {
            memset(f, 0x7f, sizeof(f));  // 因为是一维,所以每次都必须先初始化 
            for(int i=1; i<=3; i++) {
                for(int k=1; k<=3; k++) {
                    f[i] = min(f[i], pre[k] + abs(a[i][j] - a[k][j-1]));
                }
            }
            for(int t=1; t<=3; t++) {  // f数组更新完后,将其拷贝给pre数组 
                pre[t]=f[t];
            }
        }
        cout<<min(f[1], min(f[2], f[3]));
    }
    

    题目二:

    给出一个 n*m 二维矩阵 (n<=500, m<=500),矩阵的每一行和每一列都是一个独立的等差数列,其中一些数据缺失了,现在需要推理隐藏但是可以被唯一确定的数字,然后对输入的查询进行回答。

    输入描述:
    第一行,n,m,q 分别表示矩阵的行数,列数和查询的条数。
    接下来的 n 行,每行 m 个数表示这个矩阵,0表示缺失数据。
    接下来 q 行,每行两个数字 x, y 表示对矩阵中第 i 行第 j 列的数字进行查询。

    输出描述:
    如果可以确定该位置的数字,则输出该数字,如果不能确定则输出 字符串 "Unknown"。

    输入示例:

    2 3 6
    1 0 3
    0 0 0
    1 1
    1 2
    1 3
    2 1
    2 2
    2 3
    

    输出示例:

    1
    2
    3
    Unknown
    Unknown
    Unknown
    

    样例解释:
    矩阵是一个 2*3 的矩阵:[[1,0,3], [0,0,0]],总共询问 6 次。由第一行,可以确定公差为 1,因此前三次询问中,第一行的三个数都可以确定。但是,第二行是 3 个 0,不能推断出每一个数,因此后三次询问中,都输出 "Unknown"。

    解题思路:

    先将整个矩阵 a 推断出来,把能够确定的数字填入矩阵中,并用一个标记数组 vis 标记某个位置的数是否是确定的。然后再进行询问,对于确定的数直接输出结果,否则输出 "Unknown"。

    关键点在于,如何推断出这个矩阵?如果我们知道每一行有两个确定的数字,我们就可以计算出该行的公差 d;同理,如果我们知道每一列有两个确定的数字,我们也可以计算出该列的公差 d。比如某一行:0 3 0 0 9 11 0 0,我们找到 3 和 9,就可以计算出公差 d = 2,并且可以推断出这行的其他数字,因此改行可以得到 1 3 5 7 9 11 13 15。

    因此,算法步骤如下(假设是 work() 处理函数):

    • 对于每一行,去计算公差 d,如果可以计算出来,推断该行所有的数字,填充到矩阵 a 中,并用标记数组 vis 标记该行所有数字都可以推断。
    • 对于每一列,去计算公差 d,如果可以计算出来,推断该列所有的数字,填充到矩阵 a 中,并用标记数组 vis 标记该列所有数字都可以推断。

    举例:

    a 矩阵        ->          推断行                ->              推断列
    0 2 0 4                  1 2 3 4                              1 2 3 4
    1 0 5 0                  1 3 5 7                              1 3 5 7
    0 4 0 0                  0 4 0 0                              1 4 7 10
    

    但是,这种做法有一个问题,比如:

    a 矩阵        ->          推断行                ->              推断列
    0 2 0 4                  1 2 3 4                              1 2 3 4
    0 3 0 0                  0 3 0 0                              0 3 0 5
    0 0 0 6                  0 0 0 6                              0 4 0 6
    

    会发现只推断一次行于列,推断不完全!实际上,需要推断多次,才能将所有的数字全部推理出来。再比如下面的这个例子:

    7*5的矩阵,需要行列各推理三次才能全部推理结束

    黄色表示确定的数字,空白表示不确定的数字。因此,需要调用 3 次 work() 函数才能全部推理出来。

    因此,可以在代码中写一个 while(work()) ; 不断调用 work() 处理函数,在 work() 函数中加一个标记变量 flag = false,假设新一轮扫描中没有产生新的未知量,如果发现标记数组 vis 不再变化为止,说明推断结束,返回 flag (false),否则,将 flag 改为 true,继续进行下一轮的推断。

    其实,还有一个更为隐蔽的 case,就是比如某一行为 -1 0 1,这个 0 是确定的数字,并不是未知量,因此需要在找两个非 0 数字过程中将条件 if(a[i][j] != 0) 改成 if(a[i][j] != 0 || vis[i][j] == true),就可以考虑到这种情况。如:

    a 矩阵        ->          推断行              ->             推断列
    -1 0 0                  -1 0 0                              -1 -2 -3 (-2根据0推出)
    0 -1 1                  -3 -1 1                             -3 -1 1
    -5 0 5                  -5 0 5 (这个0确定)                   -5 0 5 (这个0确定) 
    

    在推断第 3 行的时候,中间的 0 是确定的数字,不是未知量,因此 vis[3][2] = true,当推断第 2 列时,根据 if(a[3][2] != 0 || vis[3][2] == true) 这个条件,条件也成立,说明可以利用 a[3][2] 上的 0,来推断出 a[1][2] = -2。

    时间复杂度为 O(k*n*m)(k 是 work() 函数调用的次数),空间复杂度也为 O(n*m)。

    C++ 实现:
    #include<iostream>
    using namespace std;
    
    int n,m,q,x,y;
    int a[505][505], vis[505][505];
    
    void work() // 填充矩阵a,并标记某个位置的数是否是确定的 
    {
        int flag = false; // 新一轮扫描中,没有产生新的未知量 
        for(int i=1; i<=n; i++) {  // 枚举每一行,检验该行上是否有两个确定的数字 
            int p = 0;  // 对于第i行,找到第一个确定的数字出现的位置p和公差d  
            int d = 0x7fffffff;
            for(int j=1; j<=m; j++) {  
                if(a[i][j]!=0 || vis[i][j]==true) {  // 后一个条件是为了处理 -1 0 1 的情况,这个0是确定的数字0 
                    if(p==0) {
                        p = j;
                    } else {  // 第二次出现确定的数字,可以计算出公差d 
                        d = (a[i][j]-a[i][p]) / (j-p);
                        break; 
                    }
                }
            }
            if(d!=0x7fffffff) {  // d计算出来了,填充第i行中的每个数 
                for(int j=1; j<=m; j++) {
                    a[i][j] = a[i][p] + d * (j-p);
                    if(vis[i][j] == false) flag = true;  //  新一轮扫描中,产生了新的未知量 
                    vis[i][j] = true;  // 标记第i行的每个数字都是确定的 
                }
            }
        }
        for(int j=1; j<=m; j++) {  // 枚举每一列,检验该列上是否有两个确定的数字 
            int p = 0;
            int d = 0x7fffffff;
            for(int i=1; i<=n; i++) {
                if(a[i][j]!=0 || vis[i][j] == true) {
                    if(p==0) {
                        p = i;
                    } else {
                        d = (a[i][j]-a[p][j]) / (i-p);
                        break; 
                    }
                }
            }
            if(d!=0x7fffffff) {
                for(int i=1; i<=n; i++) {
                    a[i][j] = a[p][j] + d * (i-p);
                    if(vis[i][j] == false) flag = true;
                    vis[i][j] = true;
                }
            }
        }   
        return flag; // 如果返回true,说明还需要继续扫描一轮 
    } 
    
    int main()
    {
        // 输入 
        cin>>n>>m>>q;
        for(int i=1; i<=n; i++) {
            for(int j=1; j<=m; j++) {
                cin>>a[i][j];
            }
        }
        // 处理
        while(work());  // 扫描多次行域列,直到vis数组不变化为止,才说明推断结束 
        // 询问q次 
        for(int i=1; i<=q; i++) {
            cin>>x>>y;
            if(vis[x][y]) {
                cout<<a[x][y]<<endl;
            } else {
                cout<<"Unknown"<<endl;
            }
        }
    } 
    

    相关文章

      网友评论

          本文标题:阿里巴巴2020暑期实习笔试题

          本文链接:https://www.haomeiwen.com/subject/xixeyhtx.html