2022-04 Task02交大ACM C++算法

c++算法初级

枚举

暴力枚举

  • 可以通过加入数学计算、并且存储尽可能多的信息的方法,来降低时间复杂度。

  • fibonacci数列问题:

  • #include<bits/stdc++.h>
    using namespace std;
    int main()
    {
    
      int a = 2,b = 3;
      int sum = 0;
      for(int i = 5;i <= 12; i++)
      {
          sum = a + b;
          a = b;
          b = sum;
    
      }
      cout << sum;
      return 0;
    }

子集枚举

  • 枚举所有子集

  • 子集的表示方式(基于枚举)

    • 01比特串法:如果01比特串的第i个元素是0,表示在该子集中没有包含第i个元素,相反如果第i个元素是1,则表示该子集中包含了第i个元素。 对于集合{1, 2, 3, 4, 5, 6}

010001表示子集{2,6}。

{1,2,3,5}表示成111010。

1、枚举整数的范围: 从0-2的n次方-1

2、如何知道第i个元素是否在集合里

可以利用位运算,从右往左 第i个元素是否在子集中

int i_th_bit = (num >> (i - 1)) & 1; // 如果i的范围是1到n

int i_th_bit = (num >> i) & 1; // 如果i的范围是0到n-1

  • Q集合中所有数求和使3的倍数的子集的个数: T:假设我们有集合{1,2,3,…,n},输出所有满足集合中所有数求和是3的倍数的子集的个数。

#include <bits/stdc++.h>
using namespace std;
int n;
int main() {
    scanf("%d", &n);    // 集合大小,也就是01比特串的长度
    int tot = 1 << n;   // 枚举数字代替01比特串,范围为0到2^n - 1  tot = 2^n - 1 (!一个数的二进制表示)
    int ans = 0;
    for (int num = 0; num < tot; ++num) {  // 枚举每个代表01比特串的数字
        long long sum = 0;
        for (int i = 0; i < n; ++i)        // 枚举01比特串的每一位, 共执行了2^n次
            if ((num >> i) & 1) {          // 二进制表示中检查第j位是否为1,注意这里是从0开始枚举, 枚举n个二进制位的每一位,时间复杂的为O(n)
                sum += (i + 1);            // 如果该位是1,就把对应的数字加到求和的变量里
            }
        if (sum % 3 == 0) ++ans;           // 如果满足题目要求(3的倍数),计入答案
    }
    printf("%d\n", ans);
}

代码时间复杂的是O(n*2^n),不是O(n^2)

  • 珠心算:集合中的数各不相同,然后要求学生回答:其中有多少个数,恰好等于集合中另外两个(不同的)数之和?

  • 
    #include<stdio.h>
    // a巧用下标法
    int main()
    {
    int n,a[100005],b[100005]={0},i,count=0,j;
    scanf("%d",&n);
    for(i=0;i<n;i++)
    {
    	scanf("%d",&a[i]);
    	b[a[i]]=1;
    }
    	for(i=0;i<n;i++)
    		for(j=i+1;j<n;j++)
    			if(b[a[i]+a[j]]==1)
    			{
    				count++;
    				b[a[i]+a[j]]=0;
    			}
    			printf("%d\n",count);
    return 0;
    }
    #include<stdio.h>
    //b暴力枚举
    int main()
    {
    int n;
    int i,j,k,a[100],count=0;
    scanf("%d",&n);
    for(i=0;i<n;i++)
    	scanf("%d",&a[i]);
    for(i=0;i<n;i++)
    	for(j=0;j<n;j++)
    		for(k=0;k<n;k++)
    			if(j!=k && (a[i]==a[j]+a[k]))
    			{
    					count++;
    					i++;
    					j=0;k=0;
    			}
    printf("%d",count);
    return 0;
    }
    #include<bits/stdc++.h>
    #define M(a) memset(a,0,sizeof(a))
    using namespace std;
    int a[1010];
    int b[1010];
    int main()
    {
    int n;
    memset(b,0,sizeof(b));
    scanf("%d",&n);
    for(int i=0;i<n;++i)
    {
        scanf("%d",&a[i]);
    }
    int num=0;
    sort(a,a+n);
    for(int q=0;q<n-2;++q)
    {
        for(int i=q+1;i<n-1;++i)
        {
            for(int j=i+1;j<n;++j)
            {
                if(b[j]==0&&a[j]==a[q]+a[i])
                {
                    ++num;
                    b[j]=1; //不设置这个标志是不行的
                   /* 
                    {
                      11
                      99 50 49 48 51 47 46 53 52 45 54
                      输出为 5
                      正确输出为1
                    }
                    */
                }
            }
        }
    }
    printf("%d\n",num);
    return 0;
    }

  • 递归枚举子集:

排列枚举

  • 取宝石问题: 八皇后问题:(深度优先广度优先搜索中会再讲解,这里只给出枚举的思路)如果我们把列看成数组的下标,行看成数组里的值的话,其中一个解的数组表示就是int a[10] = {0, 5, 7, 1, 4, 2, 8, 6, 3}; // 这里我们从1开始存储,0号无意义。 八皇后问题转换成如下问题:寻找一个1~n的排列,使得它满足八皇后问题中的对角线限制

  • 枚举问题的一般思路:

  • 1、确定枚举对象、枚举范围和判定条件;

  • 2、枚举可能的解,验证是否是问题的解。

  • 对8皇后问题:

  • 1、枚举所有排列;

  • 2、检查每一个排列是否满足要求;

    • 取宝石问题代码实现:假设在一个大房间有n个宝石,每一处宝石用一个坐标(x, y)(x,y)表示。如果你从任意一处宝石的地方出发,依次经过每个放宝石的地方并取走宝石,最终要求回到出发地点,问最短需要走的距离是多少。 解题思路:在这个情境里,经过不同地点的顺序会改变最终的行走距离。所以,我们要枚举的就是经过1~n一共n个位置的顺序。 用next_permutation函数解决: 要用枚举法解决第一个问题,所以,代入到题目的情境中,我们可以设计如下算法:

枚举所有n个点的排列

维护最短距离。检查新枚举的排列产生的行走距离是否比之前的最短距离还短。如果短,就更新答案。

#include <bits/stdc++.h>
#define N 15
using namespace std;
int n, id[N];
double x[N], y[N];

// 求两个点(x_1, y_1)和(x_2, y_2)之间的直线距离
double dis(double x_1, double y_1, double x_2, double y_2) {
    double dx = x_1 - x_2;
    double dy = y_1 - y_2;
    return sqrt(dx * dx + dy * dy);
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> x[i] >> y[i]; // 输入每个宝石的坐标
        id[i] = i;	// 因为我们枚举标号的排列,所以要将标号存进数组里
    }
    double ans = -1;    // 因为最开始ans中没有值,所以我们可以将其设置为一个不合法的值	
    // 用do...while循环是为了防止第一次调用时数组id中的值已经被重排
    // 所以会导致标号为1, 2, ..., n的排列没有被计算。
    do {
        // 求解按照id[1], id[2], ..., id[n], id[1]作为行走路线的总距离。
        double cur = dis(x[id[1]], y[id[1]], x[id[n]], y[id[n]]);
        for (int i = 1; i < n; ++i)
            cur += dis(x[id[i]], y[id[i]], x[id[i + 1]], y[id[i + 1]]);
    
        // 如果当前路线的总距离小于之前最优解,就更新。
        if (ans < 0 || cur < ans) ans = cur;
    } while (next_permutation(id + 1, id + n + 1));	

    // 输出答案,这里因为是浮点数,所以我们设置精度为4。
    cout << setprecision(4) << ans << endl;
    return 0;
}

- 八皇后问题:递归枚举排列

  • C++标准模板库Standard Template Library(STL)

    • STL分为算法algorithm、容器container和迭代器iterator三部分,实现类代码开发中常用的算法(如求最小值最大值,排序二分查找等)和数据结构(如向量vector,集合set,映射map等)

    • C++标准模板库(STL)中提供了生成数组排列序的现成实现。 next_permutation(头指针,尾指针,比较函数),该函数的作用范围包括头指针但不包括尾指针,所以尾指针要加一 int a[10] = {1, 2, 3}; next_permutation(a, a + 3); // 调用完该函数之后,数组a中的元素会重排 // 此时a数组的元素为{1, 3, 2},因为next_permutation会将a数组中的元素重排成 // 按照字典序顺序的下一个排列 返回值:表示是否存在字典序更大的排列。 以字典序的顺序排列

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a[10] = {3, 2, 1, 4};
    // TODO 请补全代码
    if (next_permutation(a, a+4)) {  // 使用``next_permutation``函数生成下一个较大的排列
        cout << "Yes" << endl;
    } else {
        cout << "No" << endl;
    }
    for (int i = 0; i < 4; ++i) cout << a[i] << ' ';
    cout << endl;
    return 0;
}

- 复杂度分析 这里我们给出右侧使用next_permutation函数枚举排列代码的复杂度分析:

do while 循环的循环次数,也就是长度为n的排列个数为n!。 调用next_permutation函数一次的复杂度为O(n) 所以调用next_permutation枚举排列的复杂度为O(n!×n)。

  • 练习题:

  • 三连击 将 1,2,…,9 共 9 个数分成三组,分别组成三个三位数,且使这三个三位数的比例是a : b : c,试求出所有满足条件的三个三位数,若无解,输出 No!!!。

  • #include<bits/stdc++.h>
    using namespace std;
    //3、使用位运算  (没理解)
    //int main()
    //{
    //    int a,b,c,r,flag = 0;
    //    int t[3];
    //    cin>>a>>b>>c;
    //    for(int i=100; i<=334;i++){
    //        if(i%a) continue;
    //        t[0] = i;
    //        t[1] = i/a *b;
    //        t[2] = i/a *c;
    //        r=0;
    //        for(int j=0;j<3;j++){
    //            int tmp = t[j];
    //            int d=0;
    //            while(tmp){
    //
    //                r |= (1<<(tmp%10-1)); //转换为二进制 且与r按位或运算 有1为1
    //                tmp /=10;
    //                d++;
    //            }
    //            if(d!=3){
    //                r=0;
    //                break;
    //            }
    //        }
    //
    //        if((r&511) == 511){
    //            flag = 1;
    //            cout<<t[0]<<" "<<t[1]<<" "<<t[2]<<endl;
    //        }
    //
    //    }
    //    if(flag == 0)
    //        cout<<"No!!!"<<endl;
    //
    //    return 0;
    //}
    //2、使用next_permutation()方法
    int main()
    {
        int s[10] = {0,1,2,3,4,5,6,7,8,9};
        int a,b,c,flag = 0;
        cin>>a>>b>>c;
        int t = a*b*c;
        a = t/a;
        b = t/b;
        c = t/c;
        do{
            if((100*s[1]+10*s[2]+s[3])*a == (100*s[4]+10*s[5]+s[6])*b && (100*s[1]+10*s[2]+s[3])*a == (100*s[7]+10*s[8]+s[9])*c)//符合比例
            {
                cout<<s[1] <<s[2] <<s[3]<< " " <<s[4]<<s[5]<<s[6]<<" "<<s[7]<<s[8]<<s[9]<<endl;
                flag++;
            }
        }while(next_permutation(s+1,s+10));
        if(flag == 0) cout<<"No!!!"<<endl;
        return 0;
    }
    
    /*
    // 最初步的版本
    int main()
    {
        int a,b,c,i,j=0;
        int s[9], flag = 0;
        cin>>a>>b>>c;
        for(i = 100; i < 1000; i++){
           if( i % a == 0){ //i是a的倍数
            int x = i/a*b; // 满足比例
            if(x < 1000){
                int y = i / a * c;
                if(y<1000){
                    if((i/100 + i/10%10 +i%10 + x/100 + x/10%10 + x%10 + y/100 +y/10%10 +y%10 == 45)&&((i/100)*(i/10%10)*(i%10)*(x/100)*(x/10%10)*(x%10)*(y/100)*(y/10%10)*(y%10) == 362880)){
                        //1到9累加和为45 1到9累乘为
                        cout<< i << " "<< x << " " << y <<endl;
                        flag = 1;
                    }
                }
            }
            }
    
            }
            if(flag == 0){
                cout<< "No!!!" <<endl;
            }
    
        return 0;
    }
    
    */

排序

1、选择排序:找最小值,与第一位元素交换

#include <bits/stdc++.h>
using namespace std;
int a[1010];
int n;

int main() {
    // 输入
    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> a[i]; 
    //注意:为保持认知一致,我们直接从数组第2个元素开始。数组第二个元素索引为1,不是从索引为0的元素开始哦。
    int min_pos = 1;    // 设置最小值位置的初始值为0,即a[1] = 0
    for (int i = 2; i <= n; ++i) {
        if (a[i] < a[min_pos])  // 比较当前枚举到的元素与当前记录位置的元素
            min_pos = i;        // 如果当前记录位置的元素更小,则更新最小值出现的位置
    }
    cout << "minimum value = " << a[min_pos] << endl;    // 输出最小值
    cout << "minimum value pos = " << min_pos << endl;   // 输出最小值的位置
    return 0;
}

选择排序是不稳定的排序算法。不稳定不稳定不稳定

2、冒泡排序

冒泡排序的思路:

1、冒泡排序分为n-1个阶段。

2、在第1个阶段,通过“冒泡”,将前n个元素的最大值移动到序列的最后一位。此时只剩前n-1个元素未排序。

3、在第i个阶段,此时序列前n-i+1个元素未排序。通过“冒泡”,我们将前n-i+1个元素中的最大值移动到最后一位。此时只剩前n-i个元素未排好序。

4、最终到第n-1个阶段,前2个元素未排序。将其中的较大值移动到后一位,则整个序列排序完毕。

#include <bits/stdc++.h>
#define N 1010
using namespace std;
int n, a[N];
int main() {
    // 输入
    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    // 冒泡排序
    for (int i = 1; i < n; ++i) { 	// 一共n-1个阶段,在第i个阶段,未排序序列长度从n-i+1到n-i。
        for (int j = 1; j <= n - i; ++j)	// 将序列从1到n-i+1的最大值,移到n-i+1的位置
            if (a[j] > a[j + 1]) 			// 其中j枚举的是前后交换元素的前一个元素序号
                swap(a[j], a[j + 1]);
    }

    // 输出
    for (int i = 1; i <= n; ++i) cout << a[i] << ' ';
    cout << endl;
    return 0;
}

稳定的算法

3、插入排序

#include <bits/stdc++.h>
#define N 1550
using namespace std;
int a[N], n;

int main() {
    // 输入
    cin >> n; 
    for (int i = 1; i <= n; ++i) cin >> a[i];
    // 插入排序
    for (int i = 2; i <= n; ++i) {    // 按照第2个到第n个的顺序依次插入
        int j, x = a[i];    // 先将i号元素用临时变量保存防止被修改。

        // 插入过程,目的是空出分界线位置j,使得所有<j的部分<=x,所有>j的部分>x。
        // 循环维持条件,j>1,并且j前面的元素>x。
        for (j = i; j > 1 && a[j - 1] > x; --j) {   //j = i   a[j-1] >x//因为要把j-1 向后移动
            // 满足循环条件,相当于分界线应向前移,
            // 分界线向前移,就等于将分界线前面>x的元素向后移
            a[j] = a[j - 1];              
                                                
        }
        // 找到分界线位置,插入待插入元素x
        a[j] = x;                         
    }

    // 输出
    for (int i = 1; i <= n; ++i) cout << a[i] << ' ';
    cout << endl;
    return 0;

}

4、快速排序

// 该代码参考 https://www.geeksforgeeks.org/quick-sort/
#include <bits/stdc++.h>
#define N 100010 
using namespace std; 
int n; 
int a[N]; 

void quick_sort(int l, int r) { 
    // 设置最右边的数为分界线
    int pivot = a[r];
    // 元素移动
    int k = l - 1;
    for (int j = l; j < r; ++j)  //j=l
        if (a[j] < pivot) swap(a[j], a[++k]); 
    swap(a[r], a[++k]); 

    if (l < k - 1) quick_sort(l, k - 1); // 如果序列的分界线左边的子段长度>1,排序
    if (k + 1 < r) quick_sort(k + 1, r); // 如果序列的分界线右边的子段长度>1,排序
    // 上面的过程结束后,到这里左子段和右子段已经分别排好序。又因为确定分界线以后的移动操作
    // 保证了左子段中的元素都小于等于分界线,左子段中的元素都大于分界线。所以整个序列也是有序的。
} 

int main() { 
    // 输入
    scanf("%d", &n); 
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); 
     // 快速排序
    quick_sort(1, n); 

    // 输出
    for (int i = 1; i <= n; ++i) printf("%d ", a[i]);  
    return 0; 
}

算法时间复杂度 O(nlogn)

空间复杂度O(n) 分治思想

关于练习题: 统计数字出现的次数,可以使用

struct node{
   int x;
   int fre;
}a[N];   

6、归并排序

归并排序实现起来也并不容易,所以STL中也有对归并排序的优化实现,函数名为stable_sort()

#include <bits/stdc++.h>
#define N 100010
using namespace std;
int n;
int a[N], b[N]; //辅助数组为b[N]

// 合并操作
void merge(int l, int r) {
    for (int i = l; i <= r; ++i) b[i] = a[i]; // 将a数组对应位置复制进辅助数组
    int mid = l + r >> 1;           // 计算两个子段的分界线
    int i = l, j = mid + 1;         // 初始化i和j两个指针分别指向两个子段的首位
    for (int k = l; k <= r; ++k) {  // 枚举原数组的对应位置
        if (j > r || i <= mid && b[i] < b[j]) a[k] = b[i++]; // 上文中列举的条件,两个有序表的合并
        else a[k] = b[j++];
}
}

void merge_sort(int l, int r) { // l和r分别代表当前排序子段在原序列中左右端点的位置
    if (l >= r) return;         // 当子段为空或者长度为1,说明它已经有序,所以退出该函数
    int mid = l + r >> 1;       // 取序列的中间位置,并将序列分成两部分(左右长度相差最多为1)
    merge_sort(l, mid);
    merge_sort(mid + 1, r);
    merge(l, r);                // 将l..mid和mid+1..r两个子段合并成完整的l..r的有序序列
}

int main() {
    // 输入
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	// 归并排序 
    merge_sort(1, n);

    // 输出
    for (int i = 1; i <= n; ++i) printf("%d ", a[i]); 
    return 0;
}

空间复杂度O(n)

时间复杂度O(nlong n)

7、记数排序

元素的范围可以被很容易转换到[0..K],我们也可以使用计数排序。如果元素范围是[A..B],我们可以通过简单的平移关系将其对应到[0..B-A]上

1、统计原序列中每个值的出现次数,记为cnt数组。

2、从小到大枚举值的范围,对cnt数组求前缀和,记为sum数组。

3、从后往前枚举每个元素a[i],分配其在答案中的位置idx[i]为当前的sum[a[i]],也就是将其放在所有值等于a[i]中的最后一个。并且将sum[a[i]]减少1,保证下次再遍历到同样的值时,它分配的位置正好在idx[i]前面一个。

#include <bits/stdc++.h>
#define N 1000005
#define K 1000001    // 假设非负整数最大元素范围为1000000
using namespace std;
int a[N], n, b[N];
int cnt[K];
int main() {
    // 输入
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        ++cnt[a[i]];    // 这里通过计数数组cnt来维护每一种值出现的次数
    }
    // 维护最终有序序列
    for (int i = 0, j = 0; i < K; ++i)      // 枚举每一种值i,指针j用来枚举填充答案数组中的位置
        for (int k = 1; k <= cnt[i]; ++k)   // 根据该值出现的次数
            b[++j] = i;                     // 添加对应个数的i到答案序列

// 输出
    for (int i = 1; i <= n; ++i)
        cout << b[i] << ' ';
    cout << endl;

    return 0;
}


    // 求原序列和答案序列中的位置对应
    // 求原序列和答案序列中的位置对应
    sum[0] = cnt[0];               // 假设最小值为0
    for (int i = 1; i < K; ++i)    // 求cnt的前缀和
        sum[i] = sum[i - 1] + cnt[i];
    for (int i = n; i; --i)        // 给每个元素分配位置
        idx[i] = sum[a[i]]--;      // 之所以倒循环,是因为对于相等的元素我们是从后向前分配位置
                               // 这样我们可以保证排序的稳定性

    // 根据求出的位置将每个元素放进答案序列中
    for (int i = 1; i <= n; ++i)
        b[idx[i]] = a[i];

    // 输出
    for (int i = 0; i <= n; ++i)
        cout << b[i] << ' ';
    cout << endl;

    return 0;
}

上述计数排序实现方法的时间和空间复杂度都是O(n+K)。

计数排序的基本思想还可以拓展成桶排序和基数排序。使用桶排序和基数排序的,可以对更大范围内的,甚至不是整数的序列进行排序。

二分查找

最多需要查找多少次呢? Log(n)

int L = 区间左端点;
int R = 区间右端点; // 闭区间
while( L < R ) { // 区间内有至少两个数字
    int M = L+(R-L)/2; // 区间中点
    if( M是答案 ) 答对啦;
    else if( M比答案小 ) L = M+1;
    else R = M-1; // M比答案大
}
// 若运行到这里,因为答案一定存在,所以一定有L==R,且L是答案

### 

二分查找方法

#include <iostream>
using namespace std;

int n, x, a[100000];

int main() {
    cin >> n >> x;
    // 输入数组
    for( int i = 0; i < n; ++i ) 
        cin >> a[i];
    // 考虑数组中不存在大于等于x的数字的情况
    if( x > a[n-1] ) {           
        cout << -1 << endl;
        return 0;
    }

// 二分查找
    int L = 0, R = n-1;          // 数组下标从0到n-1,闭区间
    while( L < R ) {             // 当区间中至少有两个数字的时候,需要继续二分
        int M = L + (R - L) / 2; // 求出区间中点
        if( a[M] < x ) {         // 答案一定出现在[M+1,R]中
            L = M+1;
         } else {                 // a[M] >= x,答案一定出现在[L,M]中
            R = M;
        }
    }

    // 此时L == R,a[L]就是第一个大于等于x的数字
    if ( a[L] == x) {
        cout << L << endl;  // 如果答案存在,则输出答案
    } else {
        cout << -1 << endl; // 如果答案不存在,则输出-1
    }

    return 0;
}

如果代码中是用的L = M,把L不断往右push,那么M向上取整(M = L + (R - L + 1)/2);

如果代码中是用的R = M,把R不断往左push,那么M向下取整(M = L + (R - L)/2)

  • 二分查找的函数:lower_bound和upper_bound。

lower_bound的用途是:在指定的升序排序的数组中,找到第一个大于等于x的数字。

upper_bound的用途是:在指定的升序排序的数组中,找到第一个大于x的数字。 左闭右开区间! 返回对应数字的指针(或者是迭代器)

在double上二分时,尽量使用固定次数二分的方法(求精确到10位小数点)

递归

递推,意思就是用已经有的信息一点点推出想要知道的信息。

应用一:一维递归

张爽的青蛙

地上有nn个石头从左到右排成一排,张爽同学养的青蛙要从第一个石头跳到最后一个石头上,每次可以选择向右跳一格或者跳两格,问总共有多少种不同的走法?

张爽的青蛙问题其实就是斐波那契数列(时间复杂度O(2^n))的一种变形。 开一个数组int f[n],其中f[i]表示从第一个石头跳到第i个石头一共有多少种方案

#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353; // 答案对998244353取模。
int k, f[1000010];

int main() {
    cin >> k;
    f[1] = f[2] = 1; // 初始条件
    for( int i = 3; i <= k; ++i )
        f[i] = (f[i-1] + f[i-2]) % MOD; // 递推式,记得取模
    cout << f[k] << endl;
    return 0;
}

卡特兰数:由n对括号组成的括号序列,有多少种是合法的括号序列?
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
int n, f[100010];

int main() {
    cin >> n;
    f[0] = 1; // 初始条件,0对括号智能组成一种序列,即空序列
    for( int i = 1; i <= n; ++i ) { // 求f[i]的值
        for( int k = 0; k < i; ++k ) {
            f[i] += int((long long)f[k] * f[i-k-1] % MOD); // 递推式
            // 注意,两个int相乘的结果可能爆int,因此乘法的过程要转换成long long以避免整数溢出
            f[i] %= MOD; // 记得取模
        }
    }
    cout << f[n] << endl;
    return 0;
}

贴瓷砖
有一块大小是 2*n 的墙面,现在需要用2种规格的瓷砖铺满,瓷砖规格分别是 2 * 1 和 2 * 2,请计算一共有多少种铺设的方法。

输入描述:

输入的第一行包含一个正整数T(T<=20),表示一共有T组数据,接着是T行数据,每行包含一个正整数N(N<=30),表示墙面的大小是2行N列。

输出描述:

输出一共有多少种铺设的方法,每组数据的输出占一行。

#include <bits/stdc++.h>
using namespace std;
// 递归方法求解
int f(int m) //大小是2*M
{
    if(m == 1){
        return 1;
    }
    if(m == 2){
        return 3;
    }
    return f(m-1) + 2*f(m-2);
}
int main() {
    // 请补全代码,实现题目功能
    int n;
    cin>>n;
    while(n--){
        int m;
        cin>>m;
        cout<<f(m)<<endl;
    }
   
    return 0;
}
也可以用动态规划的思想求解,https://blog.csdn.net/weixin_43207025/article/details/89602338
for 循环 + 数组dp

错位排列(信箱问题)

有n个信封和nn个信件,第i个信件属于第ii个信封,我们想知道,有多少种不同的方法,使得没有任何一个信件被装入正确的信封中?
递推式f[n] = (n-1)(f[n-1] + f[n-2])。
 

#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;

int f[1000010], n;

int main() {
    cin >> n;
    f[1] = 0; // 初始条件
    f[2] = 1;
    for( int i = 3; i <= n; ++i ) {
        f[i] = (long long)(i-1) * (f[i-1] + f[i-2]) % MOD;
        // 注意取模,并且小心乘法会爆int
    }
    cout << f[n] << endl;
    return 0;
}

应用而:二维递推

杨辉三角(二维递推)问题:从nn个不同的物品中选取mm个,有多少种不同的选择方法?这是一个经典的组合数问题,而本题需要你解决一个更难的问题:给出k,你需要输出一个(k+1)*(k+1)(k+1)∗(k+1)的矩阵,其中第ii行第jj列表示,从ii个不同的物品中选jj个,有多少种不同的方法(行和列的标号从0开始)。

根据左侧的描述,请尝试推测杨辉三角问题的递推公式:

f[i][j] = f[i-1][j-1] + f[i-1][j];

#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;

int k;
int f[2010][2010] = {0}; // 初始化f数组为全0
int main() {
    for( int i = 0; i <= k; ++i ) {
        f[i][0] = f[i][i] = 1; // 递推边界条件
        for( int j = 1; j < i; ++j ) {
            // TODO 请补全下述代码
            f[i][j] = (f[i-1][j-1] + f[i-1][j])%MOD; // 请补全递推式,记得取模
        }
        for( int j = 0; j <= k; ++j ) {
            cout << f[i][j] << ' '; // 输出这一整行
        }
        cout << endl;
    }
    return 0;
}

时间复杂度:O(n^2)。

一个算法的递推公式如下:f(n)=f(n/2)+nf(n)=f(n/2)+n,则该算法的时间复杂度为 O(n)
 

递归求阶乘

#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;

int f( int n ) {
    if( n == 0 )
        return 1;    // 0的阶乘等于1
    else return
        (long long)f(n-1) * n % MOD; // 注意取模,小心爆int
}

int main() {
    int n;
    cin >> n;
    cout << f(n) << endl;
    return 0;
}

写递归算法时,要牢记该函数只干一件事情,要写出所有边界条件,要放心大胆地递归调用自己。

动态规划

金字塔问题

#include <bits/stdc++.h>
#define N 1005
#define M 110
using namespace std;

int n;

int a[N][N], f[N][N];

int main() {
    // 输入
	cin >> n;
	for (int i = 1; i <= n; ++i) 
		for (int j = 1; j <= i; ++j)
			cin >> a[i][j];
			
    // 动态规划过程
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= i; ++j)
			f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];
    		// 此处没有讨论 j == 1 和 i == j 的情况
    		// 是因为当 j == 1 时,f[i - 1][j] == 0
    		// 是因为在数字金字塔所有数字都是正数的情况下
    		// max函数一定不会选择用f[i - 1][j]来转移
    		// i == j 的情况同理
			
    // 输出
	int ans = 0;
	for (int i = 1; i <= n; ++i) ans = max(ans, f[n][i]);	// 求第n行的最大值
	cout << ans << endl;
	
	return 0;
}

动态规划分析流程:
用动态规划解决问题的过程,就是一个把原问题的过程变成一个阶段性决策的过程。

比如在数字金字塔问题中,路径每往下延伸一行,我们就进行到下一个阶段,或者步骤。而在每一个步骤里,我们需要决策到底是从左上过来,还是从右上过来。在运用动态规划方法分析问题的过程中,下面四个要素是要明确的

最优化类问题都能用动态规划来解决呢?

不是。

在这里指出,用动态规划求解要求我们设计出状态和转移方程,使得它们满足下面三个条件:

1、最优子结构:原问题的最优解,必然是通过子问题的最优解得到的。比如上面的例子中,我们提过,如果所有以77为结尾的路径里面,有一条的数字和最大。那么,在所有经由77到达22的路径里,我们一定选择到达77的和最大的一条。所以,这样的问题具有最优子结构的性质。

2、无后效性:前面状态的决策不会限制到后面的决策。比如说数字金字塔问题里,无论以任何方式走到77,我们都可以在后面接一段从77走到22,变成一条到达22的路径。所以,数字金字塔没有后效性。但是,在旅行商问题里,如果我们从11号城市开始,走到33号城市,那么途中经没经过22号,将会影响到33号城市后面的路径。这个场景就是有后效性的例子。

3、重复子问题:一个子问题可以被重复利用到多个父亲状态中。我们发现在下面这张图中,f[3][2]既可以用来更新f[4][2],又可以用来更新f[4][3]。那么,因为我们把它存在数组里,所以只需要计算一次f[3][2],就可以使用很多次。也就是说,f[4][2]和f[4][3]有个共同的子问题f[3][2]。

辰辰采药在算法中属于一种很经典的0-1背包问题。更一般的,这种问题可以转化为:

给定n个物品,每个物体有个体积v_i和一个价值p_i。现有一个容量为V的背包,请问如何选择物品装入背包,使得获得的总价值最大?
 

#include <bits/stdc++.h>
#define N 1002
using namespace std;

int n, V, v[N], p[N];
int f[N][N]; 

int main() {
    // 输入
    cin >> V >> n;		// V是总体积,n是物品总个数
    for (int i = 1; i <= n; ++i) 
        cin >> v[i] >> p[i];
    
    // 动态规划
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= V; ++j) {
            if (j < v[i]) 
                f[i][j] = f[i - 1][j];	// 当前背包容量不够装第i个物品	
            else 
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + p[i]); // otherwise
        }
    } 
	
    // 输出
    cout << f[n][V] << endl;
    return 0;
}

滚动数组优化

#include <bits/stdc++.h>
#define N 1002
using namespace std;

int n = 3; 
int V = 70;
int v[N] = {0, 71, 69, 1}; // 背包中共有3个物体,体积分别为71,60,1
int p[N] = {0, 100, 1, 2}; // 背包中共有3个物体,价值分别为100,1,2
int f[2][N]; // 相当于只开了两个一维数组
int main() {
    
    // 动态规划
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= V; ++j) {
            if (j < v[i]) 
                // TODO 请补全代码
                f[i & 1][j] = f[(i - 1) & 1][j]; // 取i的奇偶性
            else 
                // TODO 请补全代码
                f[i & 1][j] = max(f[(i - 1) & 1][j], f[(i - 1) & 1][j - v[i]] + p[i]); // otherwise
        }
    } 
    
    // 输出
    cout << f[n & 1][V] << endl;
    return 0;
}

滚动算法优化2 —— 优化到一维数组

那么我们可不可以再进一步优化空间,使得只用一个一维数组就能解决整个问题了呢?
 


#include <bits/stdc++.h>
#define N 1002
using namespace std;

int n = 3; 
int V = 70;
int v[N] = {0, 71, 69, 1}; // 背包中共有3个物体,体积分别为71,60,1
int p[N] = {0, 100, 1, 2}; // 背包中共有3个物体,价值分别为100,1,2
int f[N]; // 相当于只开了一个一维数组
int main() {
    // 动态规划过程
    for (int i = 1; i <= n; ++i) {
        for (int j = V; j >= v[i]; --j) {
        // 只枚举到v[i],是因为在v[i]之前,所有f[i][j] = f[i - 1][j]
        // 那么在一维数组的场景下,就相当于没有改变
            f[j] = max(f[j], f[j - v[i]] + p[i]) ; // TODO 请补全代码
        }
    } 
    
    // 输出
    cout << f[V] << endl;
}

ACM 算法模板集 Contents 一. 常用函数与STL 二. 重要公式与定理 1. Fibonacci Number 2. Lucas Number 3. Catalan Number 4. Stirling Number(Second Kind) 5. Bell Number 6. Stirling's Approximation 7. Sum of Reciprocal Approximation 8. Young Tableau 9. 整数划分 10. 错排公式 11. 三角形内切圆半径公式 12. 三角形外接圆半径公式 13. 圆內接四边形面积公式 14. 基础数论公式 三. 大数模板,字符读入 四. 数论算法 1. Greatest Common Divisor最大公约数 2. Prime素数判断 3. Sieve Prime素数筛法 4. Module Inverse模逆元 5. Extended Euclid扩展欧几里德算法 6. Modular Linear Equation模线性方程(同余方程) 7. Chinese Remainder Theorem中国余数定理(互素于非互素) 8. Euler Function欧拉函数 9. Farey总数 9. Farey序列构造 10. Miller_Rabbin素数测试,Pollard_rho因式分解 五. 图论算法 1. 最小生成树(Kruscal算法) 2. 最小生成树(Prim算法) 3. 单源最短路径(Bellman-ford算法) 4. 单源最短路径(Dijkstra算法) 5. 全源最短路径(Folyd算法) 6. 拓扑排序 7. 网络预流和最大流 8. 网络最小费用最大流 9. 网络最大流(高度标号预流推进) 10. 最大团 11. 二分图最大匹配(匈牙利算法) 12. 带权二分图最优匹配(KM算法) 13. 强连通分量(Kosaraju算法) 14. 强连通分量(Gabow算法) 15. 无向图割边割点和双连通分量 16. 最小树形图O(N^3) 17. 最小树形图O(VE) 六. 几何算法 1. 几何模板 2. 球面上两点最短距离 3. 三点求圆心坐标 4. 三角形几个重要的点 七. 专题讨论 1. 树状数组 2. 字典树 3. 后缀树 4. 线段树 5. 并查集 6. 二叉堆 7. 逆序数(归并排序) 8. 树状DP 9. 欧拉路 10. 八数码 11. 高斯消元法 12. 字符串匹配(KMP算法) 13. 全排列,全组合 14. 二维线段树 15. 稳定婚姻匹配 16. 后缀数组 17. 左偏树 18. 标准RMQ-ST 19. 度限制最小生成树 20. 最优比率生成树(0/1分数规划) 21. 最小花费置换 22. 区间K大数 23. LCA - RMQ-ST 24. LCA – Tarjan 25. 指数型母函数 26. 指数型母函数(大数据) 27. 单词前缀树(字典树+KMP) 28. FFT(大数乘法) 29. 二分图网络最大流最小割 30. 混合图欧拉回路 31. 无源汇上下界网络流 32. 二分图最小点权覆盖 33. 带约束的轨道计数(Burnside引理) 34. 三分法求函数波峰 35. 单词计数,矩阵乘法 36. 字符串和数值hash 37. 滚动队列,前向星表示法 38. 最小点基,最小权点基
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值