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;
}