基础算法题目

概要

汇总练习基于 C++ 的算法题目,以加深印象,还便以日后查找。
 


目录

  1. 约瑟夫环问题
  2. 连续最大和问题
  3. 数字和为 (sum) 的方法数
  4. 回文序列
  5. 跳石板
  6. 合唱团

 


约瑟夫环问题

问题描述

   (0 sim n-1)(n) 个数字排成一个圆圈,从数字 (0) 开始每次从这个圆圈里删除第 (m)(m>1)(m=1) 就是顺序删除,没什么意思)个数字,然后从删除的这个数字后面继续从 (0) 开始数。求出这个圆圈最后剩下的那个数字。
 

问题求解

最普通的想法是用环链表来做,构建一个环链表,每个结点的编号为 (0, 1, cdots, n-1)。每次从当前位置向前移动 (m-1) 步,然后删除这个结点。最后剩下的即为所求。这个方法时间复杂度为 (O(mn))。下面寻求一种效率更高的方法。

高效的方法都要有巧妙的逻辑性。下面就要找出该问题的规律:

  • 第一个被删除的数为 ((m - 1) \% n).
  • 假设第二轮的开始数字为 (k),显然 (k= m \% n),那么这 (n - 1) 个数构成的约瑟夫环为 (k, k + 1, k + 2, k +3, cdots,k - 3, k - 2). 为了和初始问题呼应,做如下映射:
    egin{align}
    k & ightarrow 0 otag \
    k+1 & ightarrow 1 otag \
    k+2 & ightarrow 2 otag \
    & vdots otag \
    k-3 & ightarrow n-3 otag \
    k-2 & ightarrow n-2 otag
    end{align}

这就变成一个 (n -1) 个数的问题,如果能从 (n - 1) 个数问题的解推出 (n) 个数问题的解,从而得到一个递推公式,那么问题就解决了。假如我们已经知道了 (n -1) 个数时,最后剩下的编号为 (x),利用上述映射关系逆推,就可以得出 (n) 个数时,其对应的编号为 ((x + k) \% n). 其中 (k = m \% n). 我们只要知道对应编号是多少就行,不用按实际的操作去多想,这个推理的逻辑就是:(n-1) 个数时,最后 (x) 留了下来,如果重新编号,改为 (n) 个数,那么其留下来的号就是 ((x+k)\%n),知道这个就足够了。 下面要做的是把假设的变量 (k) 给消掉:
egin{align}
(x + k) \% n Leftrightarrow (x + (m \% n))\%n Leftrightarrow (x+m)\%n otag
end{align}
总结一下上述分析的意思:假设 (n -1) 个数的解为 (x),那么 (n) 个数的解就是 ((x+m)\%n),这就是规律,显然当只有一个数时,剩下的编号只有可能是 (0). 下面给出递推式:
egin{align}
f(n,m) = egin{cases} 0, qquad n=1 \ [f(n-1,m)+m] \%n, quad n>1 end{cases} otag
end{align}
这个算法时间复杂度只有 (O(n)),显然效率高了很多。下面给出代码实现。

#include<iostream>

using namespace std;

int Joseph(int n, int m)
{
    if(n <= 1 || m <= 1)
        return -1;  // 不合法

    int result = 0; // 先初始化
    for(int i=1; i<n; i++)
        result = (result+m)%(i+1);

    return result;
}
int main()
{
    cout<<Joseph(1000, 2)<<endl; // 如果从 1 开始编号,那就把结果 +1
    return 0;
}

引用知乎上一个问题圆桌上1000个人轮流开枪,最后活下来的是几号?,排在第一的分析很是通俗易懂。
 


连续最大和问题

问题描述

   一个数组有 (N) 个元素,求连续子数组的最大和。 例如:([-1,2,1]),和最大的连续子数组为 ([2,1]),其和为 (3).
 

问题求解

这道问题主要在思路问题上,刚开始想的确有点绕。

我们定义一个整型变量 (sum) 保存扫描过的数据的最大和,其随着扫描会动态增或减,再定义一个整型变量 (result) 保存我们想要的最终结果。随着扫描的进行,如果 (sum > result),由于我们是求最大值,当然要更新 (result). 如果 (sum<0),这时候我们可以断定需要舍弃掉之前的加和,所以令 (sum = 0),至于 (0 <= sum <= result),这时候无法断定是否达到最大连续子和,处于观望状态,此时不需要做什么,继续扫描即可。最终得到我们想要的 (result).

#include<iostream>
#include<vector>
//#include<limits.h>  //包含各种数据类型最值常量
using namespace std;

int MaxSubArray(vector<int> L)
{
    int sum = 0;
    int result = INT_MIN;  //内置最小整数

    for(vector<int>::iterator dp = L.begin(); dp < L.end(); dp++)
    {
        sum += *dp;
        if(sum > result)
            result = sum;
        if(sum < 0)
            sum = 0;
    }
    return result;
}

int main()
{
    vector<int> L = {-1, 2, 1};

    cout<<MaxSubArray(L)<<endl;

    return 0;
}

 


(sum) 的方法数

问题描述

   给定一个有 (n) 个正整数的数组 (A) 和一个整数 (sum),求选择数组 (A) 中部分数字和为 (sum) 的方案数。当两种选取方案有一个数字的下标不一样,我们就认为是不同的组成方案。例如输入 (n=5)(1leqslant n leqslant 1000)),(sum = 15)(1leqslant sum leqslant 1000)),(A = { 5, 5, 10, 2, 3}),最终结果应返回 (4).
 

问题求解

本题的求解看着也挺巧妙的。我们直接来看下面的表格,这样思路比较清晰,要问怎么想到的,反正目前我肯定想不到。

基础算法题目

  • 表格中第一行的数字由我们已知的参数 (sum) 来决定,也就是 (0sim sum)
  • 表格中第一列的数字由我们已知的参数数组 (A) 来决定,然后在第一个元素位置插入 (0),后面依次接数组 (A) 中的元素
  • 那么表格中的主体部分表示由对应行之前的子数组和为某个 (sum) 的方法数,比如 (n=10,sum=5) 时,其对应的数字为 (2),也就意味着数组 ({5,5,10}) 中的数字和为 (5) 的方法数是 (2). 我们的目标就是求出右下角数字的值。

弄清楚数字代表的意思之后,我们来搞清楚这些数字怎么推倒的。第一行是初始化的,我们需要从第二行开始一步步推。为了表示方便,下面我们用数字对 ((i,j)) 表示 ((n,sum)),使用 (value(i,j)) 表示其对应的值。这里提醒一句,(value(i,j)) 对应的数组中的值是 (A[i-1]).

  • 假如 (A[i-1] > sum[j]),即 (A[i-1]-sum[j]<0),由于数组中的数都是正数,所以数字 (A[i-1]) 不可能包含在方法数里,所以有
    egin{align} value[i][j] = value[i-1][j] otag end{align}

  • 假如(A[i-1]-sum[j] geqslant 0),此时可以分为两种情况:方法数里不包含 (A[i-1]) 和包含 (A[i-1]),不包含 (A[i-1]) 的方法数是 (value[i-1][j]),包含 (A[i-1]) 的方法数应等于子数组中 (A[0]sim A[i-1]) 中和为 (sum[j] - A[i-1]) 的数,即
    egin{align} value[i][j] = value[i-1][j] + value[i-1][sum[j] - A[i-1]] otag end{align}

有了推导公式,编程实现就简单了,直接贴代码:

#include<iostream>
#include<vector>

using namespace std;

int Solution(int n, int *A, int sum)
{
    vector<vector<int> > value(n+1, vector<int>(sum+1, 0));  //用来存储方案,用 0 填充
    value[0][0] = 1;  // 把左上角第一个数字初始化为 1

    for(int i = 1; i < n+1; i++) // i = 0 已经初始化完成
    {
        for(int j = 0; j <= sum + 1; j++)
        {
            value[i][j] = value[i-1][j];   //先把上一行数字加上
            if(A[i-1] <= j)  //如果这种情况,要加上包含 A[i-1] 的情况,注意下标
                value[i][j] += value[i-1][j-A[i-1]];
        }
    }

    return value[n][sum];  //返回我们要求的数
}

int main(){

    int n = 5, sum = 10;
    int A[n] = {5, 5, 10, 2, 3};
    cout << Solution(n, A, sum)<< endl;

    return 0;
}

 


回文序列

问题描述

  如果一个数字序列逆置之后跟原序列是一样的就称这样的数字序列为回文序列。例如:({1, 2, 1}, {15, 78, 78, 15} , {112}) 是回文序列,${1, 2, 2}, {15, 78, 87, 51} ,{112, 2, 11} 不是回文序列。现在给出一个数字序列,允许使用一种转换操作:
选择任意两个相邻的数,然后从序列移除这两个数,并用这两个数字的和插入到这两个数之前的位置(只插入一个和)。现在对于所给序列要求出最少需要多少次操作可以将其变成回文序列。
 

问题求解

这是一道网易笔试题。仔细分析,我们只要依次把数组的首尾数据进行比较即可,如果不等,就根据大小进行题目中规定的操作,并且移动指针即可。当然这样做的前提是数组中所有数字都是非负的.

#include<iostream>

using namespace std;

int Solution(int* L, int n)
{
    int result = 0;

    for(int i = 0, j = n-1; i < j; )
    {

        if(L[i] == L[j])  // 首尾元素相等,满足回文条件
        {
            i++;
            j--;
        }
        if(L[i] < L[j])  // 左边小
        {
            L[i+1] += L[i];
            i++;
            result++;
        }
        if(L[i] > L[j])  // 右边小
        {
            L[j-1] += L[j];
            j--;
            result++;
        }

    }
    return result;
}

int main()
{
    int n = 4;
    int L[] = {1, 1, 1, 3};

    cout<<Solution(L, n)<<endl;

    return 0;
}

 


跳石板

问题描述

   小易来到了一条石板路前,每块石板上从 (1) 挨着编号为:(1,2,3cdots). 这条石板路要根据特殊的规则才能前进:对于小易当前所在的编号为 (K) 的石板,小易单次只能往前跳 (K) 的一个约数(不含 (1) 和 $K $)步,即跳到 (K+X) ((X)(K) 的一个非 (1) 和本身的约数)的位置。 小易当前处在编号为 (N) 的石板,他想跳到编号恰好为 (M) 的石板去,小易想知道最少需要跳跃几次可以到达。
例如:(N = 4,M = 24)(4 ightarrow 6 ightarrow 8 ightarrow 12 ightarrow 18 ightarrow 24),于是小易最少需要跳跃 (5) 次,就可以从 (4) 号石板跳到 (24) 号石板。
 

问题求解

我们定义一个长度为 (M-N) 的数组,来保存能够跳到该石板所需要的最小步数,开始初始化为 (INT\_MAX). 下面看详解程序即可。

#include<iostream>
#include<vector>
#include<limits.h>  //包含各种数据类型最值常量
using namespace std;

vector<int> Factor(int n)
{
    vector<int> L;
    for (int i = 2; i < n; i++)
        if (n%i == 0)
            L.push_back(i);
    return L;
}


int Solution(int N, int M)
{
    vector<int> Mark(M-N+1, INT_MAX);  //用来存储到某个位置时需要最小的步数,初始化为 INT_MAX
    Mark[0] = 0; //第一个数开始,作下标记为零
    vector<int> factors;  // 用来存储当前数字的因子
    //int step = 0;
    for(int i = 0; i < M-N+1; i++)
    {
        if(Mark[i] < INT_MAX)  //之前没走到的位置不可能作为出发点
        {   //cout<<"i+N:"<<i+N<<endl;
            factors = Factor(i+N); // 第 i 个位置表示 i+N 这个数字
            if(factors.empty() == 0)    // 如果为空,则动不了了
            {
                for(vector<int>::iterator dp = factors.begin(); dp < factors.end(); dp++)
                {   //cout<<"*dp:"<<*dp<<endl;
                    if( i + *dp < M - N + 1 && Mark[i] + 1 < Mark[i + *dp])  // 在我们要求的范围内且当前步数小于以前存储的,则更新
                        Mark[i + *dp] = Mark[i] + 1;
                }
            }
        }
    }
    return Mark[M-N];  //返回我们要求的数

}

int main()
{
    int result = Solution(4, 24);
    //int L[] = {1, 1, 1, 3};
    if (result == INT_MAX)
        cout<<-1<<endl;
    else
        cout<<result<<endl;

    return 0;
}

 


合唱团

问题描述

   此题来自牛客网. 有 (n) 个学生站成一排,每个学生有一个能力值,牛牛想从这 (n) 个学生中按照顺序选取 (k) 名学生,要求相邻两个学生的位置编号的差不超过 (d),使得这 (k) 个学生的能力值的乘积最大,你能返回最大的乘积吗?

每个输入包含 (1) 个测试用例。每个测试数据的第一行包含一个整数 (n (1 leqslant n leqslant 50)),表示学生的个数,接下来的一行,包含 (n) 个整数,按顺序表示每个学生的能力值 (a_i(-50 leqslant a_i leqslant 50))。接下来的一行包含两个整数,(k)(d) ((1 leqslant k leqslant 10, 1 leqslant d leqslant 50)).
 

问题求解

解决的方法是采用动态规划(理由:1. 求解的是最优化问题;2. 可以分解为最优子结构).

  • 对该问题的分解是关键。

   从 (n) 个学生中,选择 (k) 个,可以看成是:先从 (n) 个学生里选择最后 (1) 个,然后在剩下的里选择 (k-1) 个,并且让这 (1) 个和前 (k-1) 个满足约束条件

  • 数学描述

   为了能够编程实现,需要归纳出其递推公式,而在写递推公式之前,首先又需要对其进行数学描述。记第 (k) 个人的位置为 (one),则可以用 (f[one][k]) 表示从 (n) 个人中选择 (k) 个的方案。然后,它的子问题,需要从 (one) 前面的 (left) 个人里面,选择 (k-1) 个,这里 (left) 表示 (k-1) 个人中最后一个(即第 (k-1)个)人的位置,因此,子问题可以表示成 (f[left][k-1]).

   学生能力数组记为 (arr[n+1]),第 (i) 个学生的能力值为 (arr[i]). (one) 表示最后一个人,其取值范围为 ([1,n])(left) 表示第 (k-1) 个人所处的位置,需要和第 (k) 个人的位置差不超过 (d),因此
egin{align}
max{k-1,one-d}leqslant left leqslant one-1 otag
end{align}

(n)(k) 定了之后,需要求解出 (n) 个学生选择 (k) 个能力值乘积的最大值。因为能力值有正有负,所以当 (one) 对应的学生能力值为正时,
egin{align}
f[one][k] = max{f[left][k-1]arr[i]},qquadmax{k-1,one-d}leqslant leftleqslant one-1 otag
end{align}

(one) 对应的学生能力值为负时,
egin{align}
f[one][k] = max{g[left][k-1]arr[i]} ,qquad max{k-1,one-d}leqslant left leqslant one-1 otag
end{align}
此处 (g[][]) 是存储 (n) 个选 (k) 个能力值乘积的最小值数组。

下面是代码实现:

#include<iostream>
#include<vector>

using namespace std;

int Solution(int n, int *arr, int k, int d)
{
    vector<vector<int> > F_max(n, vector<int>(k+1, 0));  //用来存储方案
    vector<vector<int> > G_min(n, vector<int>(k+1, 0));  //用来存储方案

    for(int i = 0; i < n; i++)
    {   //初始化,先把只选择 1 个的给搞定,i 即表示选择的位置
        F_max[i][1] = arr[i];
        G_min[i][1] = arr[i];
    }

    for(int i = 1; i < n; i++) // 记录最后一个数的位置
    {
        for(int j = 2; j <=k && j <= i; j++) //选择多少位,当然选择的位数要不大于 i
        {
            for(int m = max(0, i-d); m < i; m++) // 距离不能大于 d,且遍历确定倒数第二个值,获取最值
            {
                F_max[i][j] = max(F_max[i][j], max(F_max[m][j-1]*arr[i], G_min[m][j-1]*arr[i]));
                G_min[i][j] = min(G_min[i][j],min(G_min[m][j-1]*arr[i], F_max[m][j-1]*arr[i]));
            }
        }
    }

    int max_result = F_max[k-1][k];
    for(int i = k; i < n; i++)
    {
        max_result = max(max_result, F_max[i][k]);
    }
    return max_result;  //返回我们要求的数
}

int main(){

    int n = 36, k = 3, d = 31;
    int arr[n] = {7, -15, 31, 49, -44, 35, 44, -47, -23, 15, -11, 10, -21, 10, -13, 0, -20, -36,
                  22, -13, -39, -39, -31, -13, -27, -43, -6, 40, 5, -47, 35, -8, 24, -31, -24, -1};
    int max_value = Solution(n, arr, k, d);
    cout << max_value << endl;

    return 0;
}