笔试 | 字节跳动2021秋招第三场 数数组题目详解&扩展

文章首发于公众号面鲸,欢迎关注。另外由面鲸组织的每天一道高频面试题刷题挑战活动正在进行中,快来挑战吧~在这里插入图片描述

数数组

题目描述
  • 现在要生成一个数列,满足以下条件
    • 数列的长度为n,且每个数字的大小 a [ i ] a[i] a[i]满足 l < = a [ i ] < = r l<=a[i]<=r l<=a[i]<=r
    • 数组的和要能被3整除
  • 现在给定三个数 n , l , r n,l,r n,l,r,可否求出满足以上条件的数组个数,因为数组数量比较大,最终结果请mod 1e9+7
分析
  • 遇到这种问题,一看是mod 1e9+7,那么结果一定很大,应该是有一定的递推式子的。
  • b 0 b_0 b0表示[l,r]中除3余0的数的个数, b 1 b_1 b1表示[l,r]中除3余1的数的个数, b 2 b_2 b2表示[l,r]中除3余2的个数;令 a r r [ i ] [ x ] arr[i][x] arr[i][x]表示前i位除3余x的方案数,有 a r r [ 1 ] [ x ] = b x arr[1][x]=b_x arr[1][x]=bx。一种最直接计算 b x b_x bx的方法是遍历[l,r]中的所有整数,挨个统计。但是注意到[l,r]的范围可能很大,因此不能直接这么做。那有什么办法可以快速计算得到这些数呢。如果我们要统计除3余1的数的个数,那么这个数可以表示为 3 k + 1 3k+1 3k+1,进而有 l < = 3 k + 1 < = r l<=3k+1<=r l<=3k+1<=r,也就是 ( l − 1 ) / 3 < = k < = ( r − 1 ) / 3 (l-1)/3 <=k<=(r-1)/3 (l1)/3<=k<=(r1)/3,又因为k必须是整数,所以k的个数为 ( r − 1 ) / 3 (r-1)/3 (r1)/3向下取整减去 ( l − 1 ) / 3 (l-1)/3 (l1)/3向上取整再加1。同理我们可以计算得到除3余0,除3余2的元素的个数。
  • 当n=1的时候,答案就是[l,r]中能被3整除的数的个数 b 0 b_0 b0;当n=2的时候,如果第一个位置已经放好,那么第二个位置可以放的数字是有一定的限制的(因为要满足和模3等于0),具体来说,如果第一个位置放的数字除3余0的话,第二位只能放除3余0的那些数;如果第一个位置放的数字除3余1的话,第二位只能放除3余2的那些数,如果第一个位置放的数字除3余2的话,第三位只能放除3余1的那些数。同理可以类推到第 n − 1 n-1 n1位和第 n n n位的关系,最终的答案就是 a r r [ n ] [ 0 ] arr[n][0] arr[n][0]
  • 所以我们可以得到一个递推式
    • a r r [ i ] [ 0 ] = a r r [ i − 1 ] [ 0 ] ∗ b 0 + a r r [ i − 1 ] [ 1 ] ∗ b 2 + a r r [ i − 1 ] [ 2 ] ∗ b 1 arr[i][0] = arr[i-1][0] * b_{0} + arr[i-1][1] * b_{2} + arr[i-1][2] * b_{1} arr[i][0]=arr[i1][0]b0+arr[i1][1]b2+arr[i1][2]b1
    • a r r [ i ] [ 1 ] = a r r [ i − 1 ] [ 0 ] ∗ b 1 + a r r [ i − 1 ] [ 1 ] ∗ b 0 + a r r [ i − 1 ] [ 2 ] ∗ b 2 arr[i][1] = arr[i-1][0] * b_{1} + arr[i-1][1] * b_{0} + arr[i-1][2] * b_{2} arr[i][1]=arr[i1][0]b1+arr[i1][1]b0+arr[i1][2]b2
    • a r r [ i ] [ 2 ] = a r r [ i − 1 ] [ 0 ] ∗ b 2 + a r r [ i − 1 ] [ 1 ] ∗ b 1 + a r r [ i − 1 ] [ 2 ] ∗ b 0 arr[i][2] = arr[i-1][0] * b_{2} + arr[i-1][1] * b_{1} + arr[i-1][2] * b_{0} arr[i][2]=arr[i1][0]b2+arr[i1][1]b1+arr[i1][2]b0
  • 然后你就可以用时间复杂度为O(n),空间复杂度也为O(n)的代码轻松过了。
#include <bits/stdc++.h>
using namespace std;
typedef long long int ll;
const ll mod=1e9+7;
 
int main() {
    ll n,l,r; cin>>n>>l>>r;
    ll a1,a2,a0;
    a1=floor((long double)(r-1)/3.00)-ceil((long double)(l-1)/3.00)+1;
    a2=floor((long double)(r-2)/3.00)-ceil((long double)(l-2)/3.00)+1;
    a0=floor((long double)(r-3)/3.00)-ceil((long double)(l-3)/3.00)+1;
 
    vector<vector<ll>> dp(n,vector<ll>(3,0));
 
    dp[0][0]=a0; dp[0][1]=a1; dp[0][2]=a2;
 
    for(ll i=1;i<n;i++) {
        dp[i][0]=((dp[i-1][1]*a2)%mod+(dp[i-1][0]*a0)%mod+(dp[i-1][2]*a1)%mod)%mod;
        dp[i][1]=((dp[i-1][0]*a1)%mod+(dp[i-1][1]*a0)%mod+(dp[i-1][2]*a2)%mod)%mod;
        dp[i][2]=((dp[i-1][0]*a2)%mod+(dp[i-1][1]*a1)%mod+(dp[i-1][2]*a0)%mod)%mod;
    }
    cout<<dp[n-1][0]%mod;
 
    return 0;
}

这是python代码这是Java代码


你以为结束了么
  • 做到这里这个题其实是可以很轻松的通过了,但是如果 n n n特别大,导致你不能再O(n)的时间复杂度内计算出来呢,我们又该如何应对?在找应对方法之前我们先看一个很经典的问题,斐波那契数列数列求第n项。斐波那契数列满足条件 f 0 = 1 f_0=1 f0=1, f 1 = 1 f_1=1 f1=1, f n = f n − 1 + f n − 2 f_n=f_{n-1}+f_{n-2} fn=fn1+fn2,当 n > = 2 n>=2 n>=2的时候。你一定知道如何用递归的方式、循环的方式求解斐波那契数列的第n项。但是当n特别大的时候递归&循环的方式是不好使的,会超出内存&时间限制。我们可以尝试把 f n = f n − 1 + f n − 2 f_n=f_{n-1} +f_{n-2} fn=fn1+fn2写成矩阵乘法的形式,有
    [ 0 1 1 1 ] ∗ [ f n − 2 f n − 1 ] = [ f n − 1 f n ] \left[\begin{matrix}0 & 1 \\ 1 & 1\end{matrix}\right] * \left[\begin{matrix}f_{n-2} \\ f_{n-1}\end{matrix}\right] = \left[\begin{matrix}f_{n-1} \\ f_{n}\end{matrix}\right] [0111][fn2fn1]=[fn1fn]

  • 进而可以递推得到
    [ f n − 1 f n ] = [ 0 1 1 1 ] n − 1 ∗ [ f 0 f 1 ] \left[\begin{matrix}f_{n-1} \\ f_{n}\end{matrix}\right] = \left[\begin{matrix}0 & 1 \\ 1 & 1\end{matrix}\right]^{n-1} * \left[\begin{matrix}f_{0} \\ f_{1}\end{matrix}\right] [fn1fn]=[0111]n1[f0f1]

  • 所以我们只要快速求出中间这个矩阵的 n − 1 n-1 n1次幂就可以了。要求 a b a^b ab的话,如果b为偶数,可以将它分解为两部分 a b = a b / 2 ∗ a b / 2 a^b=a^{b/2} * a^{b/2} ab=ab/2ab/2;而如果b为奇数,可以表示为 a b = a b / 2 ∗ a b / 2 ∗ a a^b=a^{b/2} * a^{b/2} * a ab=ab/2ab/2a。拿b是偶数举例,之前我们计算 a b a^b ab的时候需要O(b)的时间复杂度;而观察到 a b = a b / 2 ∗ a b / 2 a^b=a^{b/2} * a^{b/2} ab=ab/2ab/2,当计算得到 a b / 2 a^{b/2} ab/2的时候,只需要再通过一次乘法就可以得到 a b a^b ab。依次类推,就可以以 l o g ( b ) log(b) log(b)的时间复杂度计算得到 a b a^b ab。这种方法就叫做快速幂。

  • a a a为矩阵的时候,同样的道理我们可以利用快速幂的思想,以log时间复杂度得到 a b a^b ab

  • 再回到这个题,前面的递推式子同样可以写成矩阵乘法的形式:
    [ b 0 b 2 b 1 b 1 b 0 b 2 b 2 b 1 b 0 ] ∗ [ a r r [ n − 1 ] [ 0 ] a r r [ n − 1 ] [ 1 ] a r r [ n − 1 ] [ 2 ] ] = [ a r r [ n ] [ 0 ] a r r [ n ] [ 1 ] a r r [ n ] [ 2 ] ] \left[ \begin{matrix} b_0 & b_2 & b_1 \\ b_1 & b_0 & b_2 \\ b_2 & b_1 & b_0 \end{matrix} \right] * \left[\begin{matrix}arr[n-1][0] \\ arr[n-1][1] \\ arr[n-1][2]\end{matrix}\right] =\left[\begin{matrix}arr[n][0] \\ arr[n][1] \\ arr[n][2]\end{matrix}\right] b0b1b2b2b0b1b1b2b0arr[n1][0]arr[n1][1]arr[n1][2]=arr[n][0]arr[n][1]arr[n][2]

  • 依次递推下去的话,可以得到
    [ a r r [ n ] [ 0 ] a r r [ n ] [ 1 ] a r r [ n ] [ 2 ] ] = [ b 0 b 2 b 1 b 1 b 0 b 2 b 2 b 1 b 0 ] n − 1 ∗ [ a r r [ 1 ] [ 0 ] a r r [ 1 ] [ 1 ] a r r [ 1 ] [ 2 ] ] \left[\begin{matrix}arr[n][0] \\ arr[n][1] \\ arr[n][2]\end{matrix}\right]= \left[ \begin{matrix} b_0 & b_2 & b_1 \\ b_1 & b_0 & b_2 \\ b_2 & b_1 & b_0 \end{matrix} \right] ^{n-1} * \left[\begin{matrix}arr[1][0] \\ arr[1][1] \\ arr[1][2]\end{matrix}\right] arr[n][0]arr[n][1]arr[n][2]=b0b1b2b2b0b1b1b2b0n1arr[1][0]arr[1][1]arr[1][2]

  • 对于中间这个矩阵的 n − 1 n-1 n1次幂,可以利用矩阵快速幂计算得到,时间复杂度是O(logn),这样,即使n很大也能很快解出来了。

#include <bits/stdc++.h>

using namespace std;

#define ll long long
const ll mod = 1e9 + 7;
struct Matrix {
    ll m[3][3];
    Matrix() {
        memset(m, 0, sizeof(m));
    }
};

Matrix mul(Matrix a, Matrix b) {
    Matrix c;
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            c.m[i][j] = 0;
            for (int k = 0; k < 3; ++k) {
                c.m[i][j] += a.m[i][k] * b.m[k][j];
                c.m[i][j] %= mod;
            }
        }
    }
    return c;
}

Matrix pow(Matrix a, ll b) {
    Matrix ret; ret.m[0][0] = ret.m[1][1] = ret.m[2][2] = 1;
    while (b) {
        if (b&1) {
            ret = mul(ret, a);
        }
        b >>= 1;
        a= mul(a, a);
    }
    return ret;

}

int main() {
    ll n, l, r; cin >> n >> l >> r;
    ll a1=floor((long double)(r-1)/3.00)-ceil((long double)(l-1)/3.00)+1;
    ll a2=floor((long double)(r-2)/3.00)-ceil((long double)(l-2)/3.00)+1;
    ll a0=floor((long double)(r-3)/3.00)-ceil((long double)(l-3)/3.00)+1;

    Matrix a;
    a.m[0][0] = a.m[1][1] = a.m[2][2] = a0;
    a.m[0][2] = a.m[1][0] = a.m[2][1] = a1;
    a.m[0][1] = a.m[1][2] = a.m[2][0] = a2;

    Matrix ret = pow(a, n-1);
    ll ans = ret.m[0][0] * a0 % mod + ret.m[0][1] * a1 % mod + ret.m[0][2] * a2 % mod;
    ans %= mod;
    cout << ans << endl;
    return 0;
}

python代码

import math
n,l,r = map(int, input().split(' '))
zero = math.floor(r/3)-math.ceil(l/3)+1
one = math.floor((r-1)/3)-math.ceil((l-1)/3)+1
two = math.floor((r-2)/3)-math.ceil((l-2)/3)+1
mod=10**9+7


class Matrix:
    def __init__(self):
        self.m = [[0 for _ in range(3)] for j in range(3)]
    def __str__(self):
        print(self.m)
        return ""

def mul(a, b):
    ret = Matrix()
    for i in range(3):
        for j in range(3):
            ret.m[i][j] = 0
            for k in range(3):
                ret.m[i][j] += a.m[i][k] * b.m[k][j]
                ret.m[i][j] %= mod
    return ret

def pow(a, b):
    ret = Matrix()
    ret.m[0][0] = 1
    ret.m[1][1] = 1
    ret.m[2][2] = 1
    while b >= 1:
        if b % 2 == 1:
            ret = mul(ret, a)
        b >>= 1
        a = mul(a, a)
    return ret

a = Matrix()
a.m[0][0], a.m[1][1], a.m[2][2] = zero, zero, zero
a.m[0][2], a.m[1][0], a.m[2][1] = one, one, one
a.m[0][1], a.m[1][2], a.m[2][0] = two, two, two
ret = pow(a, n-1)
ans = ret.m[0][0] * zero + ret.m[0][1] * one + ret.m[0][2] * two
ans %= mod
print(ans)
总结
  • 这个题如果要只是要直接通过的话是很容易的。但是当你拓展开来,发现当面临的问题规模进一步扩大的时候,就得换用别的思路来做题了,这时候就涉及到了更多的知识点,比如矩阵快速幂、矩阵乘法等等。
  • 事实上,利用矩阵转移方程求解是一类递推问题的通用解法。面鲸在这里给大家推荐一个博客,可以去看看,相信你会有更多的收获!
  • 这里给大家列了几道类似的问题,尝试去切掉它吧。
    • https://codeforces.com/contest/1105/problem/C(这个题原题)
    • https://leetcode-cn.com/problems/three-steps-problem-lcci/
    • https://codeforces.com/problemset/problem/450/B
    • https://codeforces.com/contest/1117/problem/D
号外
  • 由面鲸公众号组织的每日刷题活动正式开始啦。想要一起起飞的小伙伴欢迎后台联系小编拉你入群呀,与更多的小伙伴一起进步,奥利给!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值