Skip to content

Latest commit

 

History

History
804 lines (611 loc) · 26.8 KB

File metadata and controls

804 lines (611 loc) · 26.8 KB

一些小技巧

记录一些题目中的小算法,例如判断素数、质因数分解等

1. 质数/素数

1.1 判断素数

素数也叫质数,大于 1 的整数中,只能被 1 和其自身整除的数称为素数;1 既不是素组也不是合数

bool isPrime(int x) {
    if (x == 1) return false;
    for (int i = 2; i * i <= x; ++ i) {
        if (x % i == 0) {
            return false;
        }
    }
    return true;
}

1.2 质因数分解

质因数分解就是将一个数分解成各个质数因数,例如 12 = 2 * 2 * 3 ,具体思路为

  1. p 从 2 遍历到 x,先找到 x 的最小质因子 p
  2. 当 x == p 时,说明已经找完所有的质因子
  3. 当 x > p 时,x /= p,继续从 (1) 开始遍历
// x 是待分解的数,分解出来的质因数存储在 primes 中
// 时间复杂度 O(n)
void getPrimeFactor1(long x, vector<long>& primes) {
    long p = 2;
    while (x != 1) {
        // p 一定是第一个可以整除 x 的最小质因数
        // 这里 p 一定是质数,没必要再次判断
        for (p = 2; p <= x; ++ p) {
            if (x % p == 0) {
                break;
            }
        }
        primes.push_back(p);
        x /= p;
    }
}

// 时间复杂度 O(n)
void getPrimeFactor2(long x, vector<long>& primes) {
    long p = 2;
    for (p = 2; p <= x; ++ p) {
        // 找到 x 的一个质因子 p 之后就不断的除以 p,将 x 中所有的 p 全部找出
        while (x % p == 0) {
            primes.push_back(p);
            if (x == p)  break;
            x /= p;
        }
    }
}

// 时间复杂度 O(n^1/2)
int getPrimeFactor3(long x, vector<long>& primes) {
    for (int p = 2; p*p <= x; ++ p) {
        while (x % p == 0) {
            primes.push_back(x);
            x /= p;
        }
    }
    if (x > 1) primes.push_back(x);
}

以上的方法得到的质因数中会有重复的,如果需要得到不重复的,需要改一下代码:

void getPrimeFactor(long x, vector<long>& primes) {
    long p = 2;
    // 假设,原数已经找到了质因子(从小到大排列,不包含重复值)p1,p2...,pt
    // 此时,x = x/(p1*p2*...*pt);
    // 在 [pt+1, x] 的范围上,找 x 的最小质因数
    while (p <= x) {
        if (x % p == 0 && isPrime(p)) {
            primes.push_back(p);
            if (x == p) {
                return;
            }
            x /= p;
        }
        p += 1;
    }
}

例题:

2507. 使用质因数之和替换后可以取到的最小值

  • 参考灵神的题解,for 循环可以直接使用 i * i <= n 判断,但是要注意退出之后和 1 比较一下

  • 没必要判断是否变小,因为 $2+\frac{n}{2} \le n,(n \ge 4)$ 是一直变小的,退出条件就是 ans == n,0x3F 暴力

参考:C++实现 质因数分解、最大公约数、最小公倍数、求所有因数

1.3 筛素数

  • 埃氏筛:时间复杂度 $O(n\log\log n)$
int Eratosthenes(int n) {
    vector<int> notPrime(n+1, 0);
    notPrime[0] = notPrime[1] = 1; // 首先筛去 0 和 1
    vector<int> primes;
    
    for (int i = 2; i <= n; ++ i) {
        if (!notPrime[i]) { // i 是质数
            primes.push_back(i);
            for (int j = i; j <= n / i; ++ j) { // 避免溢出的写法
                notPimes[j*i] = 1;
            }
        }
    }
    return primes.size();
}
  • 线性筛:欧拉筛,线性筛保证每个合数只被(最小质因子)划掉一次,时间复杂度 $O(n)$
int Euler(int n) {
    vector<int> notPrime(n+1, 0);
    notPrime[0] = notPrime[1] = 1; // 首先筛去 0 和 1
    vector<int> primes;
    
    for (int i = 2; i <= n; ++ i) {
        if (!notPrime[i]) {
            primes.push_back(i);
        }
        
        // lpf[i] 表示 i 的最小质因子
        // 枚举质数 p (<= i),划掉 i * p(保证 p 是小于等于 lpf[i] 的)
        for (int p: primes) {
            if (i * p > n) break;
            notPrime[i * p] = 1;
            if (i % p == 0) break; // p 是 i 的最小质因子
        }
    }
    return primes.size(); // 返回 [0...n] 中质数个数
}

参考:0x3F-预处理质数 + 枚举

2. 最大公约数和最小公倍数

最大公约数(gcd):greatest common divisor,C++ <numeric>中有具体的库函数 std::gcd,如果自己实现可以这样写:

int gcd(int a,int b) {
    int r;
    while(b > 0) {
        r  = a % b;
        a = b;
        b = r;
    }
    return a;
}

// 递归写法,出口条件就是 b 是否为 0
int gcd(int a,int b) {
    return b>0 ? gcd(b, a%b) : a;
}

最小公倍数(lcm):lowest common multiple,C++ <numeric>中有具体的库函数 std::lcm,如果自己实现可以这样写:

// lcm(a, b) = a * b / gcd(a, b)
int lcm(int a,int b){
    return a/gcd(a,b)*b;	// 注意先除可以防溢出
}

参考:gcd函数和lcm函数(c/c++)小染大大的博客

3. Dijkstra

迪杰斯塔拉算法用来求单源最短路,也就是某个节点到其他所有节点的最短路,这里可以使用邻接矩阵或者邻接表来存图。这里以 743. 网络延迟时间 为例展示 Dijkstra 的用法:

题目给出了图的边权数组 times,图中节点数 n,指定节点 k,求节点 k 到其他节点的最大值,也就是利用 Dijkstra 求出 k 到其他节点的距离然后再取最大值

3.1 邻接矩阵

// 邻接矩阵数组,w[a][b] = c 表示 a 到 b 有权重为 c 的边
vector<vector<int>> w(n, vector<int>(n, 0));

源码:

class Solution {
    int MAX_DIST = 6010;
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        // 邻接矩阵,注意这里初始节点从 1 开始
        vector<vector<int>> w(n+1, vector<int>(n+1, 0));	
        vector<int> dist(n+1, MAX_DIST);	// dist[x] = y 表示源点到x的最短距离为 y
        vector<int> pre(n+1, 0);	// 标记最短路径的上一个节点
        
        // 初始化邻接矩阵
        for (int i = 1; i <= n; ++ i) {
            for (int j = 1; j <= n; ++ j) {
                if (i != j) w[i][j] = w[j][i] = MAX_DIST;
            }
        }
        
        // 建图,这里是有向图
        for(auto& time: times) {
            int a = time[0], b = time[1], c = time[2];
            w[a][b] = c;
        }
        
        // 求源点 k 到其他节点的最短路
        dijkstra(w, dist, pre, k, n);
		
        int ans = 0;
        for (int i = 1; i <= n; ++ i) {
            ans = max(ans, dist[i]);
        }
        // getPath(pre, k, 4);
        return ans == MAX_DIST ? -1 : ans;
    }
    
    // 邻接矩阵形式的迪杰斯塔拉算法,传入5个参数:
    // 邻接矩阵 w,到源点的最短距离数组 dist,前一个节点数组 pre,源点/起点 k,节点数 n
    void dijkstra(vector<vector<int>>& w, vector<int>& dist, vector<int>& pre, int k, int n) {
        vector<bool> vis(n+1, false);	// 标记哪些节点已经更新过了
        
        // 初始化 dist 和 pre,也可以在外面初始化 
        for (int i = 1; i <= n; ++ i) {
            dist[i] = w[k][i];
            if (dist[i] < MAX_DIST) {	// MAX_DIST 是预设的最大距离
                pre[i] = k;
            } else {
                pre[i] = 0;
            }
        }
        dist[k] = 0;
        pre[k] = k;

        // 迭代 n 次
        for (int p = 1; p <= n; ++ p) {
            // 每次找到最短距离最小且未被更新的点 u
            int u = -1;
            for (int i = 1; i <= n; ++ i) {
                if (!vis[i] && (u == -1 || dist[i] < dist[u])) u = i;
            }
            vis[u] = true;
            // 用点 u 的最小距离更新其他点
            // k-->i <==> k-->u-->i
            // 遍历 u 相邻的节点 v
            for (int i = 1; i <= n; ++ i) {
                if (!vis[i] && dist[i] > dist[u] + w[u][i]) {
                    dist[i] = dist[u] + w[u][i];
                    pre[i] = u;
                }
            }
        }
    }
};
  • 时间复杂度:两个 for 循环,$O(n^2)$
  • 空间复杂度:邻接矩阵大小,$O(n^2)$

如果要返回路径,可以遍历 pre 数组然后逆序输出

// 获取起点 start 到终点 end 的路径
// pre 是保存最短路径的上一个节点信息的数组
vector<int> getPath(vector<int>& pre, int start, int end) {
    vector<int> path;
    
    // 逆序获取路径
    int tmp = end;
    while (tmp != start) {
        path.push_back(tmp);
        tmp = pre[end];
        end = tmp;
    }
    path.push_back(start);
    
    reverse(path.begin(), path.end());
    return path;
}

3.2 邻接表

当边比较少时,使用邻接矩阵会存在浪费节点信息的情况,这时可以考虑邻接表。邻接表适用于边数较少的 「稀疏图」,当边数量接近点的数量,即 m≈n 时,可定义为「稀疏图」

// 相当于数组存储链表
// edges[u] = {{v1, c1}, {v2, c2}, {v3, c3}}
// 表示节点 u 和节点 v1, v2, v3 相邻,权重分别为 c1, c2, c3
vector<vector<pair<int, int>> edges;

源码:

class Solution {
    int MAX_DIST = 6010;
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        // 邻接矩阵,注意这里初始节点从 1 开始
        vector<vector<pair<int, int>>> edges(n+1);	
        vector<int> dist(n+1, MAX_DIST);	// dist[x] = y 表示源点到x的最短距离为 y
        vector<int> pre(n+1, 0);			// 标记最短路径的上一个节点
        
        // 建图,这里是有向图
        for(auto& time: times) {
            int a = time[0], b = time[1], c = time[2];
            edges[a].push_back({b, c});
        }
        
        // 求源点 k 到其他节点的最短路
        dijkstra(w, dist, pre, k, n);
		
        int ans = 0;
        for (int i = 1; i <= n; ++ i) {
            ans = max(ans, dist[i]);
        }
        // getPath(pre, k, 4);
        return ans == MAX_DIST ? -1 : ans;
    }
    
    // 邻接表形式的迪杰斯塔拉算法,传入5个参数:
    // 邻接表 edges,到源点的最短距离数组 dist,前一个节点数组 pre,源点/起点 k,节点数 n
    void dijkstra(vector<vector<pair<int, int>>>& edges, vector<int>& dist, vector<int>& pre, int k, int n) {
        vector<bool> vis(n+1, false);	// 标记哪些节点已经更新过了
        
        // 遍历节点 k 所有相邻的节点 y
        for (auto& [y, c]: edges[k]) {
            dist[y] = c;
            pre[y] = k;
        }
        dist[k] = 0;
        pre[k] = k;

        // 迭代 n 次
        for (int p = 1; p <= n; ++ p) {
            // 每次找到最短距离最小且未被更新的点 u
            int u = -1;
            for (int i = 1; i <= n; ++ i) {
                if (!vis[i] && (u == -1 || dist[i] < dist[u])) u = i;
            }
            vis[u] = true;
            // 用点 u 的最小距离更新其他点: k-->v <==> k-->u-->v
            // 遍历 u 相邻的节点 v
            for (auto& [v, c]: edges[u]) {
                if (!vis[v] && dist[v] > dist[u] + c) {
                    dist[v] = dist[u] + c;
                    pre[v] = u;
                }
            }
        }
    }
};
  • 时间复杂度:$O(n^2+m)$,n 为节点数,m 为边数
  • 空间复杂度:$O(mn)$

寻找最小 d[u] 的过程却可以不必达到 O(n)的复杂度,而可以使用对优化来降低复杂度。最简单的写法是直接使用 STL 中的优先队列priority_queue,这样使用邻接表实现 Dijkstra 算法的时间复杂度可以降低为 $O(n\log n+m)$

// 返回从 start 到每个点的最短路,3个参数:
// 邻接表 g, 最短路径中上一个节点的信息数组 pre, 起点 start
vector<int> dijkstra(vector<vector<pair<int, int>>>& g, vector<int>& pre, int start) {
    vector<int> dist(g.size(), MAX_DIST);
    dist[start] = 0;

    // 小顶推,在O(logn)时间内找到最小的 d[u]
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
    pq.emplace(0, start);

    while(!pq.empty()) {
        // 找到最短距离最小点 u
        auto[d, u] = pq.top();
        pq.pop();
        if (d > dist[u]) {
            continue;
        }
        // 枚举节点 u 所有相邻的节点 v
        for (auto& [v, wt]: g[u]) {
            if (dist[v] > dist[u] + wt) {
                dist[v] = dist[u] + wt;
                pq.emplace(dist[v], v);
                pre[v] = u;
            }
        }
    }
    return dist;
}

参考

相似题目

4. Floyd

佛洛依德算法用来求解多源最短路径,即每个节点到其余节点的最短路径,相当于调用 n 次 Dijkstra 算法,这里使用邻接矩阵来写 Floyd 算法,还是以 LC743 为例

class Solution {
    int MAX_DIST = 6010;
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int k) {
        // 邻接矩阵,注意这里初始节点从 1 开始
        vector<vector<int>> w(n+1, vector<int>(n+1, 0));
        // 初始化邻接矩阵
        for (int i = 1; i <= n; ++ i) {
            for (int j = 1; j <= n; ++ j) {
                if (i != j) w[i][j] = w[j][i] = MAX_DIST;
            }
        }
        
        // 建图,这里是有向图
        for(auto& time: times) {
            int a = time[0], b = time[1], c = time[2];
            w[a][b] = c;
        }
        
        // 所有节点的最短路
        floyd(w, n);
		
        int ans = 0;
        for (int i = 1; i <= n; ++ i) {
            ans = max(ans, w[k][i]);
        }
        return ans == MAX_DIST ? -1 : ans;
    }
    
    // 邻接矩阵形式的 Floyd 算法,传入2个参数:邻接矩阵 w 和节点数 n
    void floyd(vector<vector<int>>& w, int n) {
        // floyd 基本流程为三层循环:
        // 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作
        for (int p = 1; p <= n; ++ p) {
            for (int i = 1; i <= n; ++ i) {
                for (int j = 1; j <= n; ++ j) {
                    if (w[i][j] > w[i][p] + w[p][j]) {
                        w[i][j] = w[i][p] + w[p][j];
                    }
                }
            }
        }
    }
};

5. 位运算

n&(n-1)

消除数字 n 的二进制表示中的最后一个 1

  n = ... 1 000
n-1 = ... 0 111
  & = ... 0 000 

// 判断一个数是不是 2 的指数 (1 0000)
// 1 0000 & (0 1111) = 0 0000
(n & (n-1)) == 0

n&(-n)

负数用补码(反码+1)表示,所以 n&(-n) 相当于保存右侧第一个1及其右侧的数,例如:

 n = 10 1000
-n = 01 1000
 & = 00 1000   

a ^= b; b ^= a; a ^ b

交换 a 和 b,其实位运算是效率非常低的转换方式,参考

a = a ^ b;
b = b ^ a = b ^ a ^ b = a;	// b = a;
a = a ^ b = a ^ b ^ a = b;	// a = b;

6. 模运算

涉及到取模的题目,通常会用到如下等式

(a+b) mod p = (a mod p + b mod p) mod p 

# 例如
# (16 + 26) mod 10 = (16 mod 10 + 26 mod 10) mod 10 = (6+6) mod 10= 2

证明:根据带余除法,任意整数 $a$ 都可以表示为 $a=kp+r$,这里 $r$ 相当于 $a\bmod p$

那么设 $a=k_1p+r_1,\ b=k_2p+r_2$,有

$$ (a+b) \bmod p\ =((k_1+k_2) p+r_1+r_2)\bmod p\ =(r_1+r_2)\bmod p\ =(a\bmod p + b\bmod p) \bmod p $$

处理负数(a < 0):a mod p = (a mod p + p) mod p = b mod p

参考:0x3F - 前缀和+哈希表

7. Split

stringstream 通常用来格式化字符串,可以实现 cin 和 cout 的功能, 同时支持C风格字符串的输入输出操作,相关头文件是 <sstream>

  • ostringstream : 用于执行C风格字符串的输出操作,只支持 << 操作符
  • istringstream : 用于执行C风格字符串的输入操作,只支持 >> 操作符
void ostringstream_test()
{
    ostringstream oss;
    oss << "this is test" << 123456;
 
    oss.str("");//清空之前的内容
    //oss.clear();//并不能清空内存
 
    // 浮点数转换限制
    double tmp = 123.1234567890123;
    oss.precision(12);
    oss.setf(ios::fixed);// 将浮点数的位数限定为小数点之后的位数
    oss << tmp << "\r\n" << endl;
}

void istringstream_test()
{
    string str = "welcome to china";
    istringstream iss(str);
 
    // 把字符串中以空格隔开的内容提取出来
    string out;
    while(iss >> out) {
        cout << out << endl;
    }
    cout << endl;
}

// 实现 str.split(' ') 的操作
vector<string> stringstream_test(string &s, char delim)
{
    vector<string> res;
    string str;
    stringstream ss(s);
    
    while(getline(ss, str, delim)) {
        res.push_back(str);
    }
    return res;
}

另外 getline 还有如下用法:

istream& getline (char* s, streamsize n );
istream& getline (char* s, streamsize n, char delim );

8. 前缀和

对于数组 nums,定义前缀和 s[0] = 0, $s[i+1]=\Sigma_{j=0}^i nums[j]$

根据这个定义:s[i+1] = s[i] + nums[i],例如 nums = [1, 2, -1, 2],对应前缀和数组为 s = [0, 1, 3, 2, 4]

通过前缀和可以把 子数组的元素和转换成两个前缀和的差,具体如下: $$ \Sigma_{j=left}^{right}nums[j]=\Sigma_{j=0}^{right}nums[j]-\Sigma_{j=0}^{left-1}nums[j]=s[right+1]-s[left] \nonumber $$ 例如 nums 的子数组 [2, -1, 2] 的和就可以使用 s[4] - s[1] = 4 -1 = 3 计算

📢:这里相当于左闭右开区间 [left, right) 表示 nums[left] 到 nums[right-1] 的子数组,此时子数组的和为 s[right] - s[left],子数组的长度为 right - left

📢:s[0] = 0 表示一个空数组的元素和,当要计算的子数组恰好是一个从 nums[0] 开始的前缀时,使用 s[right+1] - s[0] 就可以轻松表示 nums[0] 到 nums[right] 的元素之和

前缀和 + 哈希表

题目 说明 题解
560. 和为 K 的子数组 哈希表记录前缀和出现次数 通过
974. 和可被 K 整除的子数组 哈希表记录前缀和余数出现次数 通过
1590. 使数组和能被 P 整除 哈希表记录前缀和余数首次出现下标,主要 mod 运算技巧避免出现负数 通过
523. 连续的子数组和 哈希表记录前缀和余数首次出现下标 通过
525. 连续数组 转化:0 看成 -1,哈希表记录前缀和首次出现下标 通过
面试题 17.05. 字母与数字 等价转换:数字-->1,字符-->-1 🔥 0x3F
2488. 统计中位数为 K 的子数组 等价转换:+左侧小于 -左侧大于(+1)== +右侧大于 - 右侧小于 🔥 0x3F

如果是在循环里面直接记录前缀和需要注意哈希表的初始化,一般有两种:

  • 记录 s 首次出现下标
  • 记录 s 出现次数

另外有些题目需要等价转换,例如:LC.525, LC.2488 等,遇到这些关键词:总和为 0、数量相等、轴对称、元素具有 01 二分性,就可以考虑分别给「0元素」「1元素」赋权值 -11 求和处理

9. KMP

目标串 s = "BBCXABCDABXABCDABCDABDE",模式串 p = "ABCDABD"

next 数组

  • 理解前缀和后缀的概念,ABCA 的前缀为 A、AB、ABC,后缀为 A、CA、BCA,最大公共元素长度为 1
  • next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为 -1
  • 有的博客将 next 记为 [-1 -1 -1 -1 0 1 -1],这个相当于 “最大长度值” - 1,注意在 kmp 中的使用即可

next

优化点

  • 传统的暴力匹配,s[i] != p[j] 时需要回退 i = i - (j-1), j = 0,KMP 算法在 next 数组的辅助下,当出现元素不匹配时,只需要回退 j (j != -1),并且 j = next[j],相当模式串于整体右移 j - next[j] 位

  • next[j] = k 表示 j 之前的字符串中有最大长度为 k 的相同前缀后缀

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

void GetNext(string &p, vector<int> &next)
{
    int pLen = p.size();
    next[0] = -1;
    int k = -1, j = 0;
    while (j < pLen - 1) {
        // p[k]表示前缀,p[j]表示后缀
        if (k == -1 || p[j] == p[k]) {
            ++k;
            ++j;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
}

int KmpSearch(string &s, string &p, vector<int> &next)
{
    int i = 0, j = 0;
    int sLen = s.size(), pLen = p.size();
    while (i < sLen && j < pLen) {
        // ①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++    
        if (j == -1 || s[i] == p[j]) {
            i++, j++;
        } else {
            // ②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]    
            // next[j]即为j所对应的next值      
            j = next[j];
        }
    }
    
    // 完全匹配则返回 s 中起始下标 i-j
    return j == pLen ?  i-j : -1;
}

int main() {
    string s = "BBCXABCDABXABCDABCDABDE";
    string p = "ABCDABD";

    vector<int> next(p.size());
    GetNext(p, next);
    for (int x: next) {
        cout << x << ' ';
    }
    cout << endl;

    int t = KmpSearch(s, p, next);
    if (t != -1) {
        cout << t << ' ' << s.substr(t, p.size()) << endl;    
    }

    return 0;
}

参考:

10. 后缀表达式

也叫逆波兰表达式,使用栈求值很简单,参考 剑指 Offer II 036. 后缀表达式

给定一个字符串表达式,如何求值?中缀表达式转后缀表达式是重点,如何转换?

1)如果遇到操作数,我们就直接将其输出。

2)如果遇到操作符,则我们将其放入到栈中,遇到左括号时我们也将其放入栈中。

3)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符输出直到遇到左括号为止。注意,左括号只弹出并不输出。

4)如果遇到任何其他的操作符,如 “+”, “*”,“(” 等,从栈中弹出元素直到遇到发现更低优先级的元素(或者栈为空)为止。弹出完这些元素后,才将遇到的操作符压入到栈中。有一点需要注意,只有在遇到 ")" 的情况下我们才弹出 "(",其他情况我们都不会弹出 "("。

5)如果我们读到了输入的末尾,则将栈中所有元素依次弹出。

过程参考:中缀转换后缀-CSDN

代码参考:calExp.cpp

11. 十进制转 base 进制

首先学会十进制转 -2 进制,1017. 负二进制转换

// 【分析】
//  n = …… + a * (base)^5 + b * (base)^4 + c * (base)^3 + d * (base)^2 + e * (base)^1 + f * (base)^0
//  若base为正数,则n也必定为非负数
//  若base为负数,则n可为所有整数

// 【任意base的通用写法】
string baseAny(int n, int base)
{
    if (n == 0)
        return "0";

    string ans;
    while (n != 0)
    {
        // 获取当前最低位
        int r = n % base;
        if (r < 0)     // 说明 base 必为负数
            r -= base; // 保证找到的余数在有效范围 [0, -base - 1] 内
        n -= r;        // 将最后一位抹零,保证后续是整除,从而保证不同语言通用
        if (r >= 10)
            ans += 'A' + r - 10;
        else
            ans += '0' + r;

        // 将n的进制表达式向右移一位
        n /= base;
    }
    reverse(ans.begin(), ans.end());
    return ans;
}

参考:进制表达式的本质,不同语言的通用写法