算法:
对于某一类问题,向电脑输入某些值就可以获得理想的答案。就是算法。 **需要注意哪些问题:
- 不合法的输入可能毁掉整个程序。
- 函数或者方程是有定义域的。
- 同样,输出也是需要符合题目需要的。需要考虑较多情况,并对不好的情况说明和剔除。
表示形式:
-
- 自然语言
-
- 流程图:
参考鸡兔同笼问题:
-
3. 伪代码表示法: 任何编程语言都可以看懂,并且很快ifollow想法,能较清楚的知道算法的效率。
-
- 编程语言: **return 0;可以提前结束Main函数。
算法特征与分类:
特征: 输入可描述,输出很准确。【有穷性、确切性、输入、输出、可行性。】
分类: 解决问题: 分为基本算法,数据结构相关算法,几何算法,图论算法,规划算法,数值分析算法,加密解密算法,排序算法,查找算法,并行算法,数值算法等。 确定性: 没有规范的流程但是碰到问题知道怎么去做,-不确定性问题。 思路: 针对问题选取不同的谋略,递推算法,递归算法,穷举算法,贪婪算法,分治算法,动态规划算法,迭代算法等。
计算步骤将输入转换为输出。所有输入包括合法和不合法。
数据结构:
容器---存储与处理,问题的定义与解决。
- 常见的数据结构有数组、链表、栈、队列、树、图等:
- 数组、链表、栈和队列都在描述数据的线性结构,
- 而树和图分别描述数据的树形结构和图形结构。
-
每一种数据结构利用的是数据结构中怎样的关系;
-
在不同操作上的优势劣势;
-
以及常见的使用场景
-
问题定义
- 数据之间的逻辑关系 - 逻辑与物理(存储)结构
- 关系对应的操作
-
问题解决
- 存储实现:如何存储某种逻辑关系
- 运算实现:在特定存储模式下,相关操作是如何实现的
数据的逻辑结构:
集合/线性结构/树形结构/图形结构 数据结构的操作: 创建/清除全部/插入/删除(指定节点)/检索/更新(改变某个节点)/访问(改写?)(获取节点地址)/遍历/排序。 如何利用数据之间的逻辑结构,转化成计算机上合适的的物理结构,从而达到高效进行数据处理和运算的目的。
数据的存储结构:
- 一个是数据元素的机内表示
一个数据就是一个**结点(node)** ,而数据的每一个数据项,成为该数据的**数据域(data field)** 。
- 另一个是关系的机内表示
- 一个是**顺序存储结构**
- 另一个是**链式存储结构**。
模拟算法:
扫雷比赛: 首先,随机表示雷和数字;然后用户点击位置可以表示为输入矩阵的位置,输入以后开始访问该元素,如果该元素是雷,游戏结束;如果数字等于0那么展开这个位置,怎么展开呢也就是说向上下左右直到碰到了一个>0的数字,也就是上下左右有大于0的数字。扩展的速度怎么控制呢,一次拓展是展开多少元素呢。 上下左右?逐级。因此需要一个元素表示状态。
数独比赛: 模拟我理解的就是让程序自动按照真实场景中的步骤进行。
高精度计算:
输入输出:
打印顺序和输入顺序都是按照数组的[0]开始,因为从低位地址开始输出。 但是实际生活中我们的数字高位是放在输出的第一个的。从高往低读。 计算方便使用小端从低位数开始计算。
#include <bits/stdc++.h>
using namespace std;
char str[111];
int digits[110];
int len;
int main() {
cin >> str;
int len = strlen(str);
for (int i = 0; i < len; ++i) digits[i] = str[len - i - 1] - '0';
cout << digits[0];
}
维护长度:
我有点理解为什么使用小端了,因为高位放在数组的末尾,如果运算以后高位有溢出,数组尾端可以更方便的添加删除数据,pop,push等。但是放在数组的头部,会需要整个后续数组都改变。
数位操作:
高精度计算的一个思想,数字相加,再加上前一位的进位,妙哇
- 不是并行式,是串联式,一环扣一环。
各个数据类型的具体范围。
加法进位操作
减法
- 借位操作,需要对减数和被减数加以区分。
- 使用被减数和减数长度最长的作循环。
- 考虑会不会出现负数的情况。
- 首先各个数字相减,然后向被减数的高位借位。直到遍历完。
- 更新长度,需要结果的最高位开始判断是否为零,直到找到一个不为0的数字。
或者使用加法流程。 每一位数字都变成相反数。也可行吧。
#include <bits/stdc++.h>
#define N 110
using namespace std;
// 同样,这里采用小端序存储
int a_digits[N] = {8, 6, 3, 2}, a_len = 4;
int b_digits[N] = {7, 9, 9}, b_len = 3;
int ans_digits[N], ans_len;
int main() {
// 1. 数位操作
// 我们依旧是从低位到高位开始逐位相减
// 因为我们总假设a>=b,所以初始长度先设为a的长度
// 考虑每一位,需要计算的部分是被减数的当前位,减去减数的当前位,再减去低位的借位
// 如果上一步的计算得出当前位<0,那我们需要向高位借位,然后给当前位+10
ans_len = a_len;
// 初始长度
int k = 0;
// 维护借位
for (int i = 0; i < ans_len; ++i) {
ans_digits[i] = a_digits[i] - b_digits[i] - k;
if (ans_digits[i] < 0) {
k = 1;
ans_digits[i] += 10;
} else k = 0;
// 这里赋值成0很关键,而且容易遗漏
}
// 2. 维护长度
// 想象一下,如果实际数字是1,但是长度记录是4的话,那么输出该数字结果将是0001,
// 也就是出现了“前导0”,所以维护长度的目的是为了去掉前导0
// 所以,我们用while循环实现这样的逻辑:只要最高位是0,我们就把位数缩小1位。
// 但是需要注意,只有位数>1的时候才可以缩小,否则当保存的数字是0时,长度也会减为0.
while (ans_len > 1 && !ans_digits[ans_len - 1])
// 只有长度大于1才可以去掉前导零
--ans_len;
// 3. 输出
for (int i = ans_len - 1; i >= 0; --i)cout << ans_digits[i];
cout << endl;
return 0;
}
乘法操作:
#include <bits/stdc++.h>
#define N 110
using namespace std;
int a_digits[N] = {3, 2}, a_len = 2;
int b_digits[N] = {8, 6}, b_len = 2;
// int a_digits[N] = {0}, a_len = 1;
// int b_digits[N] = {9, 9}, b_len = 2;
int ans_digits[N * 2], ans_len;
int main() {
// 1. 数位操作 // 考虑到(a位数×b位数)最多得到(a + b)位数,所以我们设置答案初始长度为a + b。
// 另外考虑到第i位×第j位会贡献到(i + j)位,所以,我们用累加的方式计算答案的每一位。
// 值得注意的是,这里累加的结果可能>=10,所以按理说应该进位,但为了效率考虑,我们
// 在后面统一维护进位,而不是一边加一边进。
ans_len = a_len + b_len;
// 初始化长度 for (int i = 0; i < ans_len; ++i) ans_digits[i] = 0;
// 因为是不断累加的形式,所以要将范围内的元素初始化为0。
for (int i = 0; i < a_len; ++i)
for (int j = 0; j < b_len; ++j)
ans_digits[i + j] += a_digits[i] * b_digits[j];
// ans的每一位更新都要使用累加的形式,这是因为对于ans的第k位,满足i + j == k的(i, j)很多,所以可能答案的第k位可能先后被更新很多次。
// 2. 统一进位
// 上一步提到,因为累加后得到的答案各个数位有可能>=10,所以要将其变成一个合法的高精度形式 // 也就是说,要把>=10的部分进位到下一位。所以我们用类似于高精度加法的方法维护。
// 每一位只需要将自己的值和低位的进位相加,然后把>=10的部分作为新的进位进到下一位。
int k = 0;
for (int i = 0; i < ans_len; ++i) {
ans_digits[i] += k;
k = ans_digits[i] / 10;
ans_digits[i] %= 10;
}
// 3. 维护长度
// 上面提到,(a位数×b位数)最多得到(a + b)位数
// 但考虑一个非零整数和0相乘的情况,答案的长度很可能降为1。所以我们需要向减法一样更新长度。
// 只有当长度仍然>1的时候,才需要去掉前导0
while (ans_len > 1 && ans_digits[ans_len - 1] == 0)
--ans_len;
// 4. 输出
for (int i = ans_len - 1; i >= 0; --i) cout << ans_digits[i];
cout << endl;
return 0;
}
#include <bits/stdc++.h>
#define N 110
using namespace std;
int a_digits[N];
int b_digits[N];
int ans_digits[N], ans_len,a_len;
char a_char[N],b_char[N];
int main() {
scanf("%s%s", &a_char,&b_char);
a_len = strlen(a_char);
ans_len = a_len;
// a_digits = a_char - '0';
// b_digits = b_char - '0';
// 初始长度
int k = 0;
// 维护借位
for (int i = 0; i < ans_len; ++i) {
ans_digits[i] = a_char[i] - b_char[i] - k;
if (ans_digits[i] < 0) {
k = 1;
ans_digits[i] += 10;
} else k = 0;
// 这里赋值成0很关键,而且容易遗漏
}
while (ans_len > 1 && !ans_digits[ans_len - 1])
// 只有长度大于1才可以去掉前导零
--ans_len;
// 3. 输出
for (int i = ans_len - 1; i >= 0; --i) cout << ans_digits[i];
cout << endl;
return 0;
}
#include <bits/stdc++.h>
#define N 110
using namespace std;
// int a_digits[N];
// int b_digits[N];
//使用for一个一个输入并计算长度;后续不变;
// char a_char[N],b_char[N];
string a_str,b_str;
int ans_digits[N], ans_len,a_len;
int main() {
// scanf("%s%s", &a_char,&b_char);
scanf("%s%s", &a_str,&b_str);
// a_len = strlen(a_char);
a_len = a_str.size();
ans_len = a_len;
// a_digits = a_char - '0';
// b_digits = b_char - '0';
// 初始长度
int k = 0;
// 维护借位
for (int i = 0; i < ans_len; ++i) {
// ans_digits[i] = a_char[i] - b_char[i] - k;
ans_digits[i] = a_str[i] - b_str[i] - k;
if (ans_digits[i] < 0) {
k = 1;
ans_digits[i] += 10;
} else k = 0;
}
while (ans_len > 1 && !ans_digits[ans_len - 1])
// 只有长度大于1才可以去掉前导零
--ans_len;
// 3. 输出
for (int i = ans_len - 1; i >= 0; --i) cout << ans_digits[i];
cout << endl;
return 0;
}
高精度算法考虑输入输出:
#include <bits/stdc++.h>
#define N 1000011
//题目要求数字大于10^500所以N应该保留这些位数比较好。
using namespace std;
//数组一次只能有一个值并且它的长度需要已知,不一定已知;
//字符串的形式输入的话需要变成数值。
int a_digits[N-1],b_digits[N-1];
int a_len,b_len;
char a_char[N],b_char[N];
int res[N-1],res_len;
//input should take the small-end mode.
void transf(char str[], int len,int num[]){
for (int t = 0; t < len; ++t){
num[t] = str[len-1-t] - '0';
}
}
int main() {
// input
scanf("%s%s",a_char,b_char);
a_len = strlen(a_char);
b_len = strlen(b_char);
transf(a_char,a_len,a_digits);
transf(b_char,b_len,b_digits);
// conduct
int k = 0;
for (int j = 0;j< a_len;j++){
res[j] = a_digits[j] - b_digits[j] - k;
if (res[j] < 0){
k = 1;
res[j] += 10;
}else k = 0;
}
res_len = a_len;
// remove 0
while (res_len > 1 && !res[res_len - 1])
--res_len;
// output
for (int m = res_len - 1; m >= 0; --m){
cout << res[m];
}
return 0;
}
答案写的减法操作时对i+1进行操作和我想的一样。
#include <bits/stdc++.h>
using namespace std;
int a[1000010], b[1000010], c[1000010], lena, lenb, lenc, i;
char n[100010], n1[100010], n2[100010];
int main(){
//1. compare two input ,and n1 is bigger.
scanf("%s", n1);
scanf("%s", n2);
if (strlen(n1) < strlen(n2) || (strlen(n1) == strlen(n2) && strcmp(n1, n2) < 0)) {
strcpy(n, n1);
strcpy(n1, n2);
strcpy(n2, n);
cout << "-";
}
// take small-end mode.
lena = strlen(n1); lenb = strlen(n2);
for(i = 0; i <= lena - 1; i++) a[lena - i] = int(n1[i] - '0');
for(i = 0; i <= lenb - 1; i++) b[lenb - i] = int(n2[i] - '0');
i = 1;
//deduction
while (i <= lena || i <= lenb) {
if (a[i] < b[i]) {
a[i] += 10;
a[i+1]--;
}
c[i] = a[i] - b[i];
i++;
}
lenc = i;
// delete 0
while ((c[lenc] == 0) && (lenc > 1)) lenc--;
// output
for (i = lenc; i >= 1; i--) cout << c[i];
cout << endl;
return 0;
}
算法评价体系:
时间复杂度:
电脑运行指令的时间,和任务的紧急需求(deadline)。
- 算法的运行时间是关于输入规模的一个函数。用表示输入规模,用表示算法的运行时间。
- 最坏时间复杂度描述的是在规模为nn的所有可能的输入中,最坏情况下算法的运行时间。
- “解决”的意思,就是在一定的限制条件下(包括时间和空间),对于所有这类问题的具体实例该算法都可以输出正确结果。
用命令行编译C++程序时,可以加入
-march=native参数会令编译器自动探测编译所在主机,并且针对该目标架构进行特定的优化,使得代码可以运行得更快。 - 代码效率评估只关心数据规模大时的代码效率。 - 估算算法效率时只保留增长最快的一项除了系数以外的部分,相当于只保留了该式子的“数量级”。
空间复杂度:
算法执行过程中所需要的空间。 常见的场景就是估算数组大小:
// 假设数据规模最大为N int a; // 常数空间复杂度 int a[N]; // 此时空间复杂度为 O(N) int a[N][N]; // 此时空间复杂度为 O(N^2)
实现复杂度:
近似算法中的近似比:
该近似算法给出的解,和严格最优解之间的差别,也就是近似比
暴力算法系列:
枚举法:计算机我就是棒棒哒头铁解法。
- 找到枚举对象和对象的范围
- 枚举俄所有的对象,并判断满足题意的对象的条件。
十进制拆分法:
1. 将第`i`位的数字移到个位。(/)
2. 提取个位。(%)
int ans = (x / pow_of_10[i]) % 10;
枚举日期
伪代码:
ans <- 0 for y <- Y_1 to Y_2 do // 枚举两个日期之间的年份,年份差不大于30 if y是闰年 then days[2] = 29 else days[2] = 28 // 调整2月份的天数 for m <- 1 to 12 do // 枚举月份 for d <- 1 to days[m] do // 根据月份确定枚举日期范围 if y年m月d日在Y_1年M_1月D_1日到Y_2年M_2月D_2日之间 then num <= y * 10000 + m * 100 + d if num % 3 == 0 then ans <- ans + 1 输出 ans
其中也可以不用先枚举所有日期后判断是否在日期区间里,可以先枚举在日期区间里的数字,不过需要分两头,把起始的月日都砍掉才可以用循环里的。这里的循环是每次判断是不是在起始日期里。
枚举区间:
不太懂,连续区间还是二维空间呢。只分三块还是其他呢。
我们枚举的对象是“绿色区间的位置”,需要检查的条件是“需要修改的格子数是否为目前最少的”。1. 枚举所有可能的绿色区间的位置[i, j][i,j]。
- 计算从原序列到目标序列需要重新涂色的格子个数Count_{i, j}Counti,j。
- 输出所有Count_{i, j}Counti,j中最小的一个。
ans <- n;
// 最多修改不会超过n个格子
for i <- 2 to n - 1 do
for j <- i to n - 1 do
cnt <- 绿色区间为[i, j]时需要重新涂颜色的格子数
if cnt < ans
then ans <- cnt
输出 ans
暴力枚举法优化策略:
加入数学计算、并且存储尽可能多的信息(前缀和数组的思想)
枚举子集:
使用比特位表示
- 01二进制字符串表示,二进制可以表示为十进制的整数,所以自然得到了元素集合所有的排列组合个数
2^n -1~0。 - 整数还原至二进制以后,类似整数取各个位数的原理,可以将二进制右移(= 十进制数字除以10并取整=
/10。 - 并且确定二进制最低位数是否为1可以使用和
00000..1进行与操作得到结果。也就是只保留对最后一位数字的确认。 - 计算所有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
int ans = 0;
for (int num = 0; num < tot; ++num) {
// 枚举每个代表01比特串的数字
long long sum = 0;
for (int i = 0; i < n; ++i)
// 枚举01比特串的每一位
if ((num >> i) & 1) {
// 检查第j位是否为1,注意这里是从0开始枚举
sum += (i + 1);
// 一共是0-n个数字。判断这些数字的排列组合。
//也可以变成n位数字,找到其中是三的倍数的数字个数。
//sum += 10^i;
// 如果该位是1,就把对应的数字加到求和的变量里
}
if (sum % 3 == 0) ++ans;
// 如果满足题目要求(3的倍数),计入答案
} printf("%d\n", ans);
}
树形结构:
数组和递归结合。一生二二生三。
它的复杂度是 其中一共有
n-1层,所以求和为。
- 如何表示子集:用01比特串表示。
- 如何枚举子集:将01比特串与其表示的整数等价,转化为简单的枚举整数的问题。
- 如何提取第
i个元素是否存在于子集的信息:位运算,具体思路为将第i位右移至第1位,转换为检查第1位的问题。检查第1位,可以将原数字与1取与运算得到。 - 这里表示子集,存储并读取二进制字符串的01和存储集合的方向一致,大端模式,也就是二进制的高位位于数组的低位。
#include <bits/stdc++.h>
#define N 105
using namespace std;
int a[N], n;
int S;
int main() {
cin >> n;
int res = 0;
int sign[n] = {0};
// first,sort this array;
// i cant sort well;
// so violent put up all;
for (int m = 0; m < n; m++){
cin >> a[m];
// cout << a[m] <<"\t";
}
// find all 3 combinations;
// not find all combination
//just note the number which
//can be the sum of other two nums;
for (int i = n-1; i >= 1; --i){
// int first = (1 << (i));
for (int j = i - 1; j >= 0; --j){
// int second = (1 << (j));
for (int k = n - 1; k >= 0;--k){
// int third = (1 << (k));
// if ((a[i] + a[j] == a[k]) || (a[j] + a[k] == a[i]) || (a[k] + a[i] == a[j])){
if ((a[i] + a[j] == a[k]) && (sign[k] == 0)){
sign[k] = 1;
//此法消去遍历过的值。
res += 1;
}
}
}
}
cout << res;
return 0;
}
理解错题意了,以为要找到所有的(x,y,z)能组成和的组合。结果只需要判断集合里的元素是不是能由其他元素求和就可了。
如果是排列组合需要找到cn3需要标记重复的元素,也就是遍历的时候需要注意标记。需要用到2^n二进制标记。如果能对所有的数字先进行排序就好了。那样遍历比较方便。节省资源。
排列枚举:
字典序:
- 从第一个字母开始比较,如果在第
i个位置满足,i没有超过两个序列的长度,小于i处两个序列对应位置的元素都相等,而第i位两个序列对应位置元素不等的话,则若a[i] < b[i],那么序列a小于序列b,否则序列b小于序列a。 - 若从第一个位置直到其中一个序列的最后一个位置都相等的话,则比较
a和b的长度,若a的长度小于b,则序列a小于序列b(此时a是b的前缀),而如果b序列的长度小于a,那么序列b小于序列a。 - 若两个序列长度相等,并且所有元素相等,则序列
a等于序列b。
标准模板库(Standard Template Library, STL) 是惠普实验室开发的一系列软件.它分为算法(algorithm)、容器(container)和迭代器(iterator)三个部分
模板和模板函数实现:
-
next_permutation函数有3个参数,分别代表头指针,尾指针和比较函数。 -
头指针和尾指针:表示该函数需要重新排列的范围是头指针到尾指针之间的所有元素,包括头指针指向的元素,不包括尾指针指向的元素。
-
比较函只有存在单个元素的大小关系,才可以定义字典序。数:这是一个可选参数,用于指定数组中存储对象的大小关系。 整数、浮点数和字符的大小关系已经在C++里面定义过了,所以不需要传比较函数。当需要对自定义的类对象数组进行重排时,可能需要传入比较函数。
-
next_permutation的返回值表示是否存在字典序更大的排列。 如果存在,返回true,否则返回false。但是即便不存在字典序最大的排列,调用该函数也会导致数组a中的元素被重排成字典序最小的一个hhO(n!*n)复杂度,其中,O(n)为调用该函数所需要的复杂度,因为该函数依据字典序对数组序列排序,所用方法不知。 -
一次函数将原数组按照下一个字典序的下一个排列;
#include <bits/stdc++.h>
#define N 15
using namespace std;
int n = 4; // 4个宝石
int id[N] = {0, 1, 2, 3, 4, 0};
double x[N] = {0, 2, 2, 3, 5, 0}; // 宝石的x坐标
double y[N] = {0, 1, 4, 5, 6, 0}; // 宝石的y坐标
// 求两个点(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() {
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]]);
// 原数组头尾是0
for (int i = 1; i < n; ++i)
cur += dis(x[id[i]], y[id[i]], x[id[i + 1]], y[id[i + 1]]);
// 如果当前路线的总距离小于之前最优解,就更新。
// 不用对res初始化为第一个排列的结果
if (ans < 0 || cur < ans) ans = cur;
// 用``next_permutation``枚举所有n个点的排列
// TODO 请补全代码
} while ( next_permutation(id+1,id+n+1));
//最大字典序退出。
// 输出答案,这里因为是浮点数,所以我们设置精度为4。
cout << setprecision(4) << ans << endl;
return 0;
}
其中,
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std ;
int a,b,c;
int main(){
int num[9] = {1,2,3,4,5,6,7,8,9};
cin >> a >> b >> c;
int res = 0;
do{
int first = num[0]*100 + num[1]*10 +num[2];
int second = num[3]*100 + num[4]*10 +num[5];
int third = num[6]*100 + num[7]*10 +num[8];
if ((first*b == second*a)&&(second*c == third*b)){
res = 1;
cout << first << " ";
cout << second <<" ";
cout << third << endl;
}
}while(next_permutation(num,num+9));
if (res == 0){
cout <<"No!!!";
}
return 0 ;
}
排序算法:
(sorting algorithm)时间的针脚。按照某个规则。
选择排序:
#include <bits/stdc++.h>
int a[1010];
#include <cstdio>
//输入输出io
#include <cstring>
//string
#include <iostream>
//标准函数
#include <algorithm>
//算法库
#define N 110
using namespace std;
int a[N], n, cnt;
int main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i)
scanf("%d", &a[i]);
// TODO 请补全下述代码,使用选择排序完成排序
for(int i = 0; i < n; i++){
int min_pos = i;
for(int j = i+1; j < n; j++){
if (a[j] < a[min_pos])
min_pos = j;
}
//找到了当前的min_pos
swap(a[i],a[min_pos]);
}
cnt = 0;
for (int i = 0; i < n; ++i)
if (i == 0 || a[i] != a[i - 1])
//直到找到一个不相同的数字。
a[cnt++] = a[i];
//cnt直接可以当结果,先赋值然后更新+1
printf("%d\n", cnt);
for (int i = 0; i < cnt; ++i)
printf("%d ", a[i]);
return 0;
}
算法流程
- 首先,确定我们要排好序以后,各个元素的位置0-i-n;
- 对于当前位置i,确定需要遍历的位置,遍历找到符合该位置的元素目前的位置j;
- 将i与j位置互相调换。直至位置n的元素也确定下来。
冒泡排序:
- 无须遍历所有就可以找到最优解。
- 一共经历n-1个阶段,其中i阶段,此时n-i+1个元素没有排序。
- 如果不知道元素的所有长度,此方法还可以确定元素的长度。
#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;
}
- 由于冒泡每一次可能都会交换两个元素,选择排序只有min_pos确定更新时,才会交换。
- 叙述过程:
- 冒泡排序分为
n-1个阶段。 - 在第
1个阶段,通过“冒泡” ,我们将前n个元素的最大值移动到序列的最后一位。此时只剩前n-1个元素未排序。 - 在第
i个阶段,此时序列前n-i+1个元素未排序。通过“冒泡” ,我们将前n-i+1个元素中的最大值移动到最后一位。此时只剩前n-i个元素未排好序。 - 最终到第
n-1个阶段,前2个元素未排序。我们将其中的较大值移动到后一位,则整个序列排序完毕。
- 其中内层元素从起始元素到终止元素-1,因为保证其j+1在数组内。
插入排序:
将第
i个元素插入到前i-1个元素的有序序列中,形成长度为i的有序序列。
// 插入排序
//由于i=1开始表示元素。
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) {
// 满足循环条件,相当于分界线应向前移, // 分界线向前移,就等于将分界线前面>x的元素向后移
a[j] = a[j - 1];
} // 找到分界线位置,插入待插入元素x
a[j] = x;
}
- 与我的想法不同的是,与其确定哪些位置要填哪些元素,还可以确定分割线。
- 插入排序的基本思想就是不断扩展有序序列的长度。 插入操作的基本思想就是从后向前不断“试探”分界线的位置。 一个合法的分界线,分界线前的元素需满足小于等于新元素大小,分界线后元素需满足大于新元素大小。所以寻找分界线的过程,就是不断把当前在分界线前,但本应该在分界线后的元素向后移动。
快速排序-原地排序-Quicksort:
sort函数有三个参数,分别为头指针、尾指针和比较函数
是C++标准模板库(STL)中一种对快速排序的优化实现
bool cmp(int x, int y) {
// 比较函数,函数的参数是当前比较的两个数组中的元素
return x > y; // x和y分别为排序数组中的两个元素。
} // 当函数返回值为true时,x应该排在y的前面。
sort(a,a+n,cmp);
- 在排序之前,先把整个数组随机打乱顺序。
- 在选取分界线时,与之前固定选取某个位置的方法相比,我们换成随机选择分界线的位置。
// 该代码参考 https://www.geeksforgeeks.org/quick-sort/
#include <bits/stdc++.h>
#define N 100010
using namespace std;
int n;
int a[N];
// 因为在int main前面所以变量为全局变量。
// 可以在函数里面使用。
void quick_sort(int l, int r) {
// 设置最右边的数为分界线
int pivot = a[r];
// 元素移动
int k = l - 1;
for (int j = l; j < r; ++j)
if (a[j] < pivot) swap(a[j], a[++k]);
swap(a[r], a[++k]);
//swap表示元素调换,其中不改变当前指针的位置,只改变对应元素。
//只要还有一个左端或者右端有数字就还继续排序,直到左右端点都停止。
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;
}
- 过程就像是从left指针开始空出位置填入元素。遍历left 和right(right元素不涉及) 指针之间的所有元素。然后找到属于小于right元素的分界线,将分界线和right元素交换。
- 使用了两个遍历指针,分别是遍历left-right之间元素的指针,一个是用于表示小于right元素边界的指针。
归并算法:
基于分治法和归并操作形成的排序算法,我们将其称为归并排序。
#include <bits/stdc++.h>
#define N 100010
using namespace std;
int n;
int a[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;
//二进制左移并整除2
// 计算两个子段的分界线
int i = l, j = mid + 1;
// 初始化i和j两个指针分别指向两个子段的首位,通常首段表示元素的最小值。
//表示合并两个方向的元素。
//l和r表示需要合并的元素范围
for (int k = l; k <= r; ++k) {
// 枚举原数组的对应位置
if (j > r || i <= mid && b[i] < b[j]) a[k] = b[i++];
//如果其中一个数组的指针已经指到了最后一个元素
//那么就把另一个数组的其余元素都填充
// 上文中列举的条件 e
else a[k] = b[j++];
//另一个矩阵也是。
}
}
void merge_sort(int l, int r) {
// l和r分别代表当前排序子段在原序列中左右端点的位置
if (l >= r) return;
// 当子段为空或者长度为1,说明它已经有序,所以退出该函数 11111quit
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;
}
STL中也有对归并排序的优化实现,函数名为stable_sort
稳定性描述的是对于有重复元素的序列,如果排序前后,重复的元素相对位置不变。
在每一层中,问题拆分的复杂度是O(1)O(1) ,这是因为我们只是单纯分解,并没有枚举或者移动元素 子段解的合并,其复杂度是O(n)O(n) 归并排序的复杂度在任何情况下都是O(n\log n)O(nlogn)。
计数排序:
计数排序算法:
K的范围比较小(例如10^6106,开长度为10^6106左右的int类型数组所占用的内存空间只有不到4M)
- 🔠input ,count the times of it appearances.
- output, according to its count ,print number for these times.
- n is the sum , K is the space of these nums.
magic tips: you can change the K,even the [start,end],using plus, deduct, streatching...
- (value,key), when it comes to the pos "one to one",NO! cant get the result
- or calculate the using F allocation funcion.
直接排序:
获得cnt[]按照顺序排列好的就比较困难。
sum[x]的含义是“小于等于x的数字个数”,同时,也可以看作指向答案序列中最后一个x出现的位置的指针。
从后向前枚举原序列
值相同的情况下,在原序列中出现在后面的元素会被分配到更大的位置,也就保证列排序的稳定性。
所有基于比较的排序算法的时间复杂度都为\Omega(n\log n)Ω(nlogn)。(\OmegaΩ和OO记号类似,但OO表示的是“不会超过”,而\OmegaΩ表示的是“不会少于”)。 不是基于比较的排序cnt[]很难求。 使用i进行排序。基于i进行
-
统计原序列中每个值的出现次数,记为
cnt数组。 -
从小到大枚举值的范围,对
cnt数组求前缀和,记为sum数组。 -
从后往前枚举每个元素
a[i],分配其在答案中的位置idx[i]为当前的sum[a[i]],也就是将其放在所有值等于a[i]中的最后一个。并且将sum[a[i]]减少1,保证下次再遍历到同样的值时,它分配的位置正好在idx[i]前面一个。
// 维护最终有序序列
for (int i = 0, j = 0; i < K; ++i)
// 枚举每一种值i,指针j用来枚举填充答案数组中的位置
for (int k = 1; k <= cnt[i]; ++k)
// 根据该值出现的次数
b[++j] = i;
// 添加对应个数的i到答案序列
// 求原序列和答案序列中的位置对应
// 假设最小值为0
sum[0] = cnt[0];
for (int i = 1; i < K; ++i) // 求cnt的前缀和
sum[i] = cnt[i] + sum[i-1] ;
for (int i = n; i; --i) // 给每个元素分配位置
idx[i] = sum[a[i]]--;
//sum[a[i]]--;不可
//找到i索引对应的值
//相同的元素,大的位被放大的
// 之所以倒循环,是因为对于相等的元素我们是从后向前分配位置
// 这样我们可以保证排序的稳定性
// 根据求出的位置将每个元素放进答案序列中
for (int i = 1; i <= n; ++i)
b[idx[i]] = a[i];
-
在计数排序的输入部分,我们用
cnt数组统计了每种值出现的个数。 -
在维护最终有序序列的部分,我们按照值从小到大的顺序,放置相应
cnt个元素到答案数组里。 时间和空间复杂度都是O(n+K)O(n+K) 。 -
计数排序的基本思想还可以拓展成桶排序和基数排序。使用桶排序和基数排序的,可以对更大范围内的,甚至不是整数的序列进行排序。
-
cnt数组的好处是它的遍历顺序是各个元素的值,而不是各个序号。
#include <bits/stdc++.h>
using namespace std;
#define N 101
#define K 1001
int a[N],n;
int cnt[K];
int main(){
int res = 0;
//input
cin >> n;
for(int i = 0; i < n; i++){
cin >> a[i];
if (cnt[a[i]] != 1){
res++;
cnt[a[i]] = 1;
}
//ony for input ,so it s well.
}
cout << res << endl;
for(int i = 0; i < K; i++){
if (cnt[i] == 1)
cout << i<<' ';
}
return 0;
}
筒排序
使用桶存储各个元素的值
二分查找:
中点的思想:
int L = 区间左端点;
int R = 区间右端点;
// 闭区间
while( L < R ) {
// 区间内有至少两个数字
int M = L+(R-L)/2;
//L = R + 1时,得到的M = L;
// 区间中点
if( M是答案 ) 答对啦;
//答对啦怎么保存答案呢
else if( M比答案小 )
L = M+1; else R = M-1;
// M比答案大
}
// 若运行到这里,因为答案一定存在,所以一定有L==R,且L是答案
//如果答案不存在那么需要比较答案是否是L;
当我们询问M位置的时候,主持人告诉我们的信息实际上帮助我们排除了一部分错误答案,从而缩小正确答案所在的区间长度。可能恰巧缩小了区间长度的一半。直到xx次后只剩下最后一个数字,就是我们想要的答案啦。也就是最后剩下一个元素;
- 二分法首先针对已经排号序号的数组
- 二分查找的原理:每次排除掉一半答案,使可能的答案区间快速缩小。
- 二分查找的时间复杂度:,因为每次询问会使可行区间的长度变为原来的一半。值取大。
是因为算法设计的目的是为了解决一类问题,而如果更明确地阐述清楚“解决”的意思,就是在一定的限制条件下(包括时间和空间),对于所有这类问题的具体实例该算法都可以输出正确结果。所以,我们只需要评估该算法在这类问题中“最难的实例”解决问题的时间,并且保证该时间不超过合理的范围内即可。
- 比如:答案不存在的情况我们是如何处理的? **使用最后一个元素L = R == x?
- 比如:当区间内只有两个数字的时候,这段代码还能正常运行吗? **可以,此时计算出来的M = L
- 比如:数组中有很多个重复元素的时候,这段代码还能正常运行吗? **可以,因为最后只剩下L,R,并通过X和其的位置关系最终锁定离答案最近的一个元素。
- 比如:为什么循环结束之后一定有
L == R?为什么不会出现L > R的情况? 因为除以2时得到的M >= L; 变形方法,从右向左数找到第一个小于等于原数字的元素,原来是从左往右数。 类似可以变形,比如数组时从大到小排列,或者等等,需要有两个因素可以改变,分别时M = L+(R-L)/2 +1 代码表示选择R作为M; 答案: M = L + (R - L + 1)/2 或者在判断语句中改变条件。
总结出一个不费脑子、不需要思考就可以写出优美代码的做法。
int L = 0,R = n-1;
while (L<R){
int M = L+(R-L)/2;
if(a[M] <= x){
L = M+1;
}else R = M;
}
我的想法是相同的数字时L需要向右移直到和右端点相同。 答案的想法是,需要找到一种情况当答案在[M+1,R]中也是需要向右移动指针,也就是当M和x相同的时候,也需要向右移动指针。
int L = 0, R = n-1; while( L < R ) { int M = L + (R - L)/2; if( a[M] <= x ) { // 答案一定在[M,R]中 L = M; } else { // 答案一定在[L,M - 1]中 R = M - 1; } } // a[L]就是答案
差一点”问题(off-by-one)
-
- 如果代码中是用的
L = M,把L不断往右push,那么M向上取整(M = L + (R - L + 1)/2);也就是从右至左的第一个。 - 如果代码中是用的
R = M,把R不断往左push,那么M向下取整(M = L + (R - L)/2)。也就是从左至右的第一个--- 左或右的第一个
- 如果代码中是用的
- 找最后一个满足条件的数字,一般是
L = M。 - 找第一个满足条件的数字,一般是
R = M。 取整方向要和参数更新方向一致,如果是M取R,那么M不相等时要将作为M的R向L方向步进一步,来更新指针。同时只需要让L=R就可以更新L了; 如果是M取L,那么R要向指针靠拢,指最后只剩下L,R两个元素时。
- -还要注意判断条件是
#include <bits/stdc++.h>
#define N 10001
using namespace std;
int a[N], n, x;
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
{
// scanf("%d", a[i]);
cin >> a[i];
}
cin >> x;
int L = 0;
int R = n -1;
while(L < R){
//L = R时退出查询
int M = L+(R-L)/2;
if(a[M] < x) L = M+1;
else R = M;
}
if (L == x) {cout << L;}a
else cout <<"not find";
return 0;
}
应用范围:
有序:数组中的元素是满足 单调性 二分查找的本质:通过比较数组中间那个值和我们要求的值的关系,来判断出“答案不可能出现在数组的某一半”,从而让我们的查找范围缩小为原来的一半。只有这样才可以准确的对某个区间的元素进行全盘否决。
- 定义日期比较函数:
struct Date { int year, month, day; };
bool operator<( const Date &a, const Date &b ) {
if( a.year == b.year ) {
if( a.month == b.month ) {
return a.day < b.day;
} else {
return a.month < b.month;
}
} else { return a.year < b.year; }
}
-
字符串也是一个天然有序的数据结构:字典序就是字符串的大小顺序。
-
二维数据:
struct Point { int x, y; };
// 这是运算符重载,当我们在代码中用小于号比较两个Point类变量的时候,就会用这个函数进行比较
bool operator<( const Point &a, const Point &b ) {
// 如何定义a < b
if( a.x == b.x ) {
return a.y < b.y;
} else { return a.x < b.x;
}
}
字符串等类似顺序数据结构比较优化方法
- 在有效范围内截取某些值,根据它来排序。
- 定义排序函数。 与之类似对于含有多个元素比较大小,需要定义一个比较函数。取中点思想,先是取中间值,然后步步取整,最后得到符合规则的数据。
- 比较时各个元素有优先级
- 如果是查找具有区间范围的数据,需要找到第一个和最后一个。那么就是使用两个指针吗?
- 字典序还需要考虑字符串的个数如果比较函数都相同的话,那么就是一个字符和一个空字符相比,结果一定是比空字符大吗?
链表和数组之间的区别:
- 存储方式:链式或者顺序方式存储
- 存储的元素是否有序:元素有序与元素无序(单不单调) 所有的有序数据结构都可以二分查找:首先元素有序然后是存储方式也是顺序的有序的。
- 链式属于线性数据结构但不是顺序存储数据结构。线性应该表示的是指针一对一或者指针一对多。(我猜的...
二分法求函数解和小精度数字
- 求单调函数的解:
(1e-10)尽量不使用double精度的方法,而是使用次数的 方法。
double f( double x ) {
return x * x * x + 16; // 某个函数f(x)
}
double solve() {
double L = -1e9, R = 1e9; // 方程解在[L,R]之间,且函数在[L,R]上单调增
while( R - L >= 1e-6 ) { // 精确到6位小数,然后四舍五入
// TODO 请补全二分查找的代码
double M = L+ (R - L) /2;
if( f(M) < 0) L = M + 1e-6;
else R = M;
}
return L;
}
此处加1会导致死循环。
- 二分法变形:除了R-L指针之间的阈值以外还可以使用二分的次数阈值。因为二分的次数和精度有关。每次都把区间长度缩短至一半,所以不仅可以提高查找的速度,也可以提高数据的精度。 - 在double上二分时,尽量使用固定次数二分的方法。 由于double本身在1e-10的基础上有很大的数字误差,所以在此使用其他操作会使精度大大丢失。
double L = -1e9, R = 1e9;
for( int times = 0; times < 100; ++times ) {
// 二分100次
double mid = (L+R)/2;
// 此处省略二分内容
}
double本身存在不小的精度误差
使用二分查找方法的c++模板函数lower_bound/upper_bound:
template
lower_bound的用途是:在指定的升序排序的数组中,找到第一个大于等于x的数字。
upper_bound的用途是:在指定的升序排序的数组中,找到第一个大于x的数字。
左闭右开区间,而且必须是升序排列,否则变成其他问题。
lower_bound
lower_bound( ForwardIt first, ForwardIt last, const T& value, Compare comp );
bool pred(const Type1 &a, const Type2 &b);
comp | - | binary predicate which returns true
int a[100000], n;
cin >> n;
for( int i = 0; i < n; ++i ) cin >> a[i];
sort(a, a + n);
int *p = lower_bound(a, a + n, 13);
// 第一个大于等于13的数字
int *q = upper_bound(a, a + n, 13);
// 第一个大于13的数字
//找到最后一个等于x的数字
int *final = upper_bound(a,a+n,13)-1;
//数组中有几个数字x
//先判断数组中是否有这个数字
if(a[p] == x)
//就算重复数字的个数
int num = q- p;
比较数学函数开立方和二分查找法的关系,需要知道函数的计算原理。 printf("%f",l)没有输入scanf中的& ** bool 函数不能判断double >0.0 ** while循环结束条件和允许进循环的条件,for函数也是都是允许进!!
#include <bits/stdc++.h>
using namespace std;
int n,m;
int main(){
cin >> n >> m;
int a[n],q[m];
for(int i = 0; i<n; i++){
cin >> a[i];
}
sort(a,a+n);
//一定记得排序。
for (int j = 0; j<m; j++){
cin >> q[j];
}
//头指针
int *p = a;
for(int j = 0; j<m; j++){
int *m = lower_bound(a,a+n,q[j]);
if (m >= a && m < a+n && (*m == q[j])) cout << int (m-p + 1) << endl;
else cout << "-1" << endl;
}
return 0;
}
- 记得
sort函数先排好序 - **- lower_bound输出为一个指针,指针不能直接int输出,输出
int *m为原值,可以仿照教程中的两个指针之差,先定义数组头元素指针,然后细节操作。 指针-指针=两个指针的间距(隔几个元素 - 判断指针的位置。需要在区间内
-
Iterator pointing to the first element that is *not less* thanvalue, orlastif no such element is found.由于区间是前开后闭,所以Last应该是数组最后一个元素对应的指针。 - 忘记判断当前元素是不是等于要找的元素了。
递推和递归
上楼梯的多种走法:
首先:算法描述:
递推式:f(n) = f(n-1) + f(n-2) 重点是n与n-1和n-2之间的迭代关系。
初始条件:也就是我们需要找到最开始的地方;
递推过程:根据初始条件和递推式找到需要的结果;
使用数组可以查询序号对应的数组元素很方便,而且递推的顺序是从小到大的;
卡特兰数:
什么是合法的括号序列?其定义如下:
- 空序列是合法的括号序列
- 如果A是合法的括号序列,那么(A)是合法的括号序列
- 如果A和B是合法的括号序列,那么AB也是合法的括号序列 其余序列由这些序列组合而成。 不是判断序列是否合法而是判断序列有多少个。对于每一个左括号与之匹配最近的右括号,如果所有的括号都能恰好匹配,那么这个序列就是合法的;
分三种情况: 括号相反,那么多0; 括号都在一侧,那么多1; 括号在外面扩起来,那么2n+1;X 考虑建立数学模型为A(B)类型,那么该问题可以简化为分类AB最终获得结果的问题。
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
int n = 5;
int f[100010];
int main() {
f[0] = 1; // 初始条件
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-1-k] % MOD);
//先对long long 进行变换,然后求和。
// 注意,两个int相乘的结果可能爆int,因此乘法的过程要转换成long long以避免整数溢出
f[i] %= MOD; // 记得取模
//因为乘了两次吗
}
}
cout << f[n] << endl;
return 0;
}
使用递推方法,递归式需要使用两层的结果,不是简单的一层累加: 内层和外层都使用数组获得元素。
-
递推思想:
根据已有的东西一点点地推出未知的东西。
-
使用递推解题三步骤:
- 数学建模
- 找出递推式和初始条件
- 写出代码。
-
因为层层递进所以需要把各个n之前的i的值都计算出来,才能获得最终结果。
-
%MOD是为什么呢
#include <bits/stdc++.h>
#define N 47
using namespace std;
int f[N],n;
int main() {
cin >> n;
if (n >46) cout << '0';
//超出46就不可以继续输出了
else{
f[1] = 1;
f[2] = 1;
for(int i =3; i<=n;i++ ){
f[i] = f[i-1] + f[i-2];
}
cout << f[n] <<endl;
}
return 0;
}
N个小朋友互相换礼物问题:
数学建模第一步: 忽略了分类讨论时,另一种情况就不会i存在的问题了 所以可以对某个小朋友X的礼物送给了Y,那么Y的礼物会送给X还是除了X以外的人。缺少了必要的模拟。 第二步:找到递推式和初始条件。 第三步:写代码。 遇到多个递推的使用多重循环,比如对于元素累加,就需要里层找元素,外层累加元素。
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;
}
杨辉三角-多维递推分类讨论问题:
第一步:数学建模 杨辉三角的行列分别代表了i表示礼物的个数,j表示小朋友的个数; f[i][j]其实表示的是他的组合数;所以我们可以用其他数来表示这个数,就是递归的思想。
- 选1号物品:由于1号物品是一定要选进来的,因此我们还剩i-1个物品,我们要从中选出j-1个物品,方案数是
f[i-1][j-1]。 - 不选1号物品:我们还剩i-1个物品,但是1号一定不选,因此我们还要从剩下的i-1个物品中选出j个物品,方案数是
f[i-1][j]。
第二步递推式和边界条件和递推顺序:
也就是头元素和对角线元素等于0;
同时注意递推中数据传递的方向: 按行按列或者旋转跳跃。
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
int k = 10;
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 ) {
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;
}
`return 0;可以提前退出程序
递归函数:
- 正确步骤:
2. 新知
-
递归函数其实是拥有相同代码处理程序和函数名字的不同的函数,其分配的地址空间也不相同,输入输出基本也不相同。新生儿各不相同,但都会长大上学结婚生娃生老病死。
-
每次我们调用函数
f()的时候,都会依照模板生产一个新的零件,名字叫“函数f()”。我们调用了很多次函数f(),也就是生产了很多名字相同的零件,它们的模样也相同,但是它们是不同的零件,因为我对一个零件操作不会影响到其他零件。那会改变什么呢?
只要函数中有自己的调用就是递归函数。感觉像是黑洞一样。
-
理解起来就像是递推方法,找到所有元素的递推公式。
-
怎样理解它的时间复杂度,其实考虑函数一直递推到某一个点终止,然后将该值返回到所调用的各层函数中,所以一来一回。
-
V形拨开我的心
-
逐层进入,逐层返回。
3. -const int MOD = 998244353;牛皮的数字;
质数奇数还是符合勾股定理。998244353
防止多项式暴int
证明递归算法的正确性,可以使用数学归纳法,也就是除了边界条件以外的输入元素,我们的公式都是横成立的,也证明了算法的正确。
-
对于任意的递归函数,我们都可以从数学归纳法的角度去理解它,再抽象一点,就是:
-
这个函数设计出来的目的是什么?
求斐波那契数列。
-
在边界条件上,这个函数正确吗?
正确。
-
这个函数能正确利用递归处理出来的结果吗?
可以正确使用。
-
因此这个函数是正确的。
不要让一个函数干两件事情!
只有每个函数干唯一指定的事情,才能保证写出来的代码是可靠易读易修改的。
在一个函数递归调用自己的时候,我们可以直接假设这个函数是一个已经写完,并且功能完善的函数,它能保质保量地完成我们想让它做的事情(这其实就是数学归纳法的前提假设)。 千万不要把递归函数展开,千万不要尝试研究递归的过程是什么样的,因为人脑根本无法承受如此大的数据量,我们只能用数学归纳法来保证递归函数的正确性。
-
复杂度:边界条件调用次数或者递归调用次数斐波纳妾数列就是调用次数的总和就约等于他的复杂度。粗略估计,可以将每一个函数单独挑出来,看看其靠近调用边界条件的次数。
-
确认并牢记这个递归函数的功能,始终不能改。
-
仔细检查,写对边界条件。
-
递归调用自己时,放心大胆地用,假设这个函数就是对的。
先尝试读懂别人的递归代码,再尝试自己设计递归算法。相信你一定能掌握递归的奥妙。
汉诺塔
首先我要遍历所有次数把塔叠好。
然后边界条件:
F[1] A -> C; F[2]A->B;A->C;B->C;
开始递归:
F[3] 先把F[2]元素都放在C上,然后A->B;C->B;C->A;B->A;B->C;A->B;A->C;B->C
对于所有元素:第n个。
在F[n-1]从A->C的基础上,A->B;F[N-1]从C->A;B->C;F[n-1]从A->C; 以F[3]举例子: 其中,F[2]从A->C;A->B;F[2]从C->A;B->C;
所以要写两个递归函数吗;
F[2] C->B;C->A;B->A;和F[2]A->B;A->C;B->C;公式交换一个AC
过程太繁琐了 递归函数不一定要return只需要走完指定的步骤就可了
#include <bits/stdc++.h>
using namespace std;
int n;
char A = 'A';
char B = 'B';
char C = 'C';
void MOVE(char a, char b){
cout << a << ' ' << "->"<< ' ' << b << endl;
//"->"string;
}
void hannoh(int n,char a,char c){
if (n == 1){
MOVE(a, c);
}
if(n == 2){
MOVE(a,B);
MOVE(a,c);
MOVE(B,c);
}
// cannot use so many return;
else{
hannoh(n-1,a,c);
MOVE(a,B);
hannoh(n-1,c,a);
MOVE(B,c);
hannoh(n-1,a,c);
}
}
int main()
{
cin >> n;
hannoh(n,A,C);
return 0;
}
官方题解 想法是先把f[n-1]从A移动到B; 把第n块从A移动到C; 然后把第f[n-1]从B移动到C; 他的命名很有generality
//汉诺塔递归函数
#include <iostream>
using namespace std;
void hanoi(int N, char source, char relay, char destination) {
if(N == 1)
cout << source << " -> " << destination << endl;
else {
hanoi(N-1, source, destination, relay);
cout << source << " -> " << destination << endl;
hanoi(N-1, relay, source, destination);
}
}
int main() {
int n;
cin >> n;
hanoi(n, 'A' , 'B' , 'C');
return 0;
}
求两个正整数的最大公约数:
求两个正整数m n的最大公约数。
已知:当m > n且m % n != 0时,gcd(m, n) = gcd(n, m % n),其中gcd(m, n)表示m n的最大公约数。
递归条件题目已经给出:
所以先找到两个数之间最大的数字。
边界条件:m%n == 0,结果就是较小的数字,n。
#include <bits/stdc++.h>
using namespace std;
int a, b;
int res = 1;
int gcd(int m, int n)
{
int max_m = max(m,n);
int min_n = min(m,n);
//函数首先默认保证大小关系
if (max_m % min_n != 0)
{
return gcd(n, m % n);
}
else
{
return min_n;
//直到相等时,返回都有的公因子就是最大公因子
}
}
int main()
{
cin >> a >> b;
int res = gcd(a,b);
cout << res;
return 0;
}
动态规划问题:
最优化问题:
最优化问题(optimization problem)是在一些约束下,通过进行一些决策,使得最终获益最大(或损失最小)的一类问题。
与旅行商问题类似,但是这个路径时固定的,只需要选择某一层的元素,最终计算的不是路程距离而是,经过的各个点的价值总和。 限制:由上到下层层遍历,而且下一层的元素需要与上一层元素的横坐标是相邻的。
给定一个nn层的金字塔,求一条从最高点到底层任意点的路径使得路径经过的数字之和最大。
注:每一步可以走到左下方的点也可以到达右下方的点。
第一步:具体表示要求的目标值,(不是有多少条路从起点开始,而是在终点处记得成绩,找到所有终点中最大值。
第二步:找到各个终点成绩的计算方式。 迭代并只保留当前节点的最大值。 f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j]; // otherwise
迭代具体实现的时候要知道那些数据已知了,由已知数据推导最终结果。 考虑多重循环等。二维的数据迭代比较复杂,需要考虑数组的边界和数组的初始状态。
int a[N][N] = {{0}, {0, 7}, {0, 3, 8}, {0, 8, 1, 0}, {0, 2, 7, 4, 4}, {0, 4, 5, 2, 6, 5}};
int f[N][N];
// 动态规划过程
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= i; ++j)
if (i == j) {
f[i][j] = f[i-1][j-1] + a[i][j];
} else if (j == 1) {
f[i][1] = f[i-1][1] + a[i][j];
} else {
f[i][j] = max(f[i-1][j],f[i-1][j-1]) + a[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;
- 用动态规划解决问题的过程,就是一个把原问题的过程变成一个阶段性决策的过程。每往下延伸一行,我们就进行到下一个阶段
- 状态。状态用于描述每一个步骤的参数以及结果。在数字金字塔的例子中,每个
f[i][j]表示的就是一个状态。其中数组下标是当前路径的结尾,而值是以i行j列元素为结尾的所有路径中的最大值。 - 转移方程。转移方程用于描述不同状态之间的关系。在上面的例子中,
f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j]就是一条转移方程。它描述了结尾为下一行的第j个结点的路径,和以上一行第j-1个结点和第j个结点路径之间的关系。 - 初始状态。初始状态描述的是整个转移方程推导的开始,是不需要经由别的状态就知道结果的状态。上面的例子中,
f[1][1]=a[i][j]就是初始状态。我们以这个状态为起点,最终推导出整个三角形上每一个位置的答案。 - 转移方向。转移方向描述的是推导出不同状态的解的先后关系。我们之所以要明确转移方向,是因为我们不希望"已知B状态只能由A状态推到过来。但是当我们想推导B时,发现A状态的结果我们还不知道”类似的事情发生。比如由转移方程中
f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j],我们发现,如果想推导f[i][j],必须先推导f[i - 1][j - 1]和f[i - 1][j]。所以,按照i从小到大,j从小到大的顺序推导是一种可行的推导方向。
-
条件:
最优子结构:原问题的最优解,必然是通过子问题的最优解得到的。 无后效性:前面状态的决策不会限制到后面的决策。
重复子问题:一个子问题可以被重复利用到多个父亲状态中。 它在实现的过程中,不得不存储产生过程中的各种状态,但是时间比较块。这也就是与迭代函数的不同,迭代函数只返回某一时刻的值,如果调用需要重头计算。
#include <bits/stdc++.h>
#define N 100000
using namespace std;
int b[N]; // 用于存储当前位置元素的最大字段和
b[0] = a[0];
//完成动态规划过程
for(int j = 1; j <n; j++){
b[j] = max(b[j-1]+a[j],a[j]);
//比较由当前j位置出发到a[0]元素的最优值。每个元素只向前组合元素
//其实求出所有相邻元素排列组合后得到的最优和。
//所以我们选择的方向为由该元素到0点元素之间的组合,由此可以遍历获得所有的组合。
//类似于杨辉三角排列组合的思想。
//而且分情况讨论,是与前一个元素连接还是不连接。
}
// 输出
int ans = 0;
for (int i = 0; i < n; ++i)
ans = max(ans, b[i]); // 求第n行的最大值
cout << ans << endl;
return 0;
}
最长子序列Longest_increasing_sequence:
-
字符子串和字符子序列: 子串必须是原串中连续的字符集合。 子序列必须保留各个元素在原字符串中的顺序。
-
动态规划求解:DP 首先,对各个状态建模:由于保留原有顺序所以我们先确定状态传递方向:和上一题一样,元素从0开始,虽然潜意识告诉我从尾部开始向头部传递,但是两者步骤都相似,都可以实现完全的遍历。 或者对于每一个元素都从自己向尾部找最长的序列。动态规划可以将遍历变成传递减少遍历次数,改善算法。
3.每个状态i的长度,由当前元素是否比前一个元素大,如果大的话,那么长度加1,如果小的话那么跳过该元素。每个状态都只存在一个最优解,因为他就像一道门槛,只考虑前面比它小的长度。(12345和567891011)当前元素是6,(后面元素是789101112131415)那么最长的元素应该包括6。但是此法不行。那么一次循环我们无法获得结果我们可以两次循环。 第一次循环获得该元素i前面的res,该res先比较元素j(j<i)是否比自己大,如果比自己小那么可以更新当前的
-
初始状态:
f[0]=1, -
当前状态:
f[i],for(int j = 0;j<i;j++){ if (a[j] < a[i]){ f[i] = max(f[i],a[j]); f[i]++;
总结就是找到该元素之前的最长上升序列,如果最长上升序列末尾数字小于自己,那么就可以得到比当前元素小的最长上升序列长度。 然后遍历所有元素的最长长度,找到整个序列的最长长度。
那么由于不确定当前元素是否是最后我们可以采取如果当前元素比最长序列中的元素小进行替换,那么原长度继续保持,如果这个小元素后续可以发展成为答案,那么将会题换掉后面的大元素。由于替换不会改变从大局出发的当前最优解,还保留了可能性。 替换的时候由于是有序序列还可以使用二分查找的方法找到结果。
#include <bits/stdc++.h>
#define N 1001
using namespace std;
int a[N],r[N];
int n;
int main() {
// input
cin >> n;
for(int i = 0; i<n; i++){
cin >> a[i];
}
//dp
r[0] = 1;
for(int i = 1; i< n; i++){
r[i] = 0;
for(int j = 0; j < i; j++){
if(a[j] < a[i]){
r[i] = max(r[i],r[j]);
}
}
r[i] += 1;
}
//output
int ans = 0;
for(int i = 0; i<n; i++){
ans = max(r[i], ans);
}
cout << ans << endl;
return 0;
}
贪心和二分法结合 无须遍历找到数组的最大值,遍历到最后一个元素可以得到最优的结果。 使用贪心算法也就是找到一个更好的解可以替换调原来的解。 由于数组以后的元素是随机的,不知道当前解是不是最好解,但是我们可以将可能potenial在数组内更新,也就是找到一个potenial可以把它将比它大的元素替换掉。 好处是结果是正确的。 坏处是无法保存结果对应的有序子序列。
算法流程: 遍历数组的每一个元素对应的位置: 初始状态:f[0] = 1;而且所有的值初始化应该为INF最大值因为交换到 用一个数组来保存最长子序列,其长度就是最长自序列的个数,但是里面不一定是子序列。 low[0]=i; 每一个状态的建模:
if (a[i] < low[j]) low(lower_bound(low,low+j,a[i])) = a[i]; //不能改变元素应该是替换 if(a[i] > low[j],low[j+1] = a[i]
转移方向,从0-n-1最后一个元素。
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int num[10]={3,6,3,2,4,6,7,5,4,3};
const int INF=0x3f3f3f3f;
int l=10, g[100], d[100];
int main()
{
fill(g, g+l, INF);
int max_=-1;
for(int i=0; i<l; i++)
{
int j = lower_bound(g, g+l, num[i]) - g;
//找到答案序列里面的数字的顺序。
d[i] = j+1;
//当前元素的最好解的长度,因为ie要加入i元素了所以要加1;
if(max_<d[i])
//如果当前解比较好就更新答案。
max_=d[i];
g[j] = num[i];
//将当前元素添加到答案序列
}
printf("%d\n", max_);
return 0;
}
这个算法其实已经不是DP了,有点像贪心。至于复杂度降低其实是因为这个算法里面用到了二分搜索。
本来有N个数要处理是O(n),每次计算要查找N次还是O(n),一共就是O(n^2);
现在搜索换成了O(logn)的二分搜索,总的复杂度就变为O(nlogn)了。
这里主要注意一下lower_bound函数的应用,注意减去的g是地址。
地址 - 地址 = 下标。
0-1背包问题:
参考背包九讲
-
状态设计:当前的状态的价值可以分为选择当前元素或者不选择当前元素两种情况。如果从前向后推就是环环相扣,与以上动态规划问题不同的是,该问题每个元素对应的状态改变的比较多,比如容量改变了,不仅仅是背包里元素的改变。 同时,当前的状态表示f[i,v]表示容量为v的背包装入i个元素的最高收益。
-
转移方程: 也就是
f[i,v] = max(f[i-1,v],f[i-1,v-w[i]]+p[i])选了该元素就不能选择其他的元素了。 -
初始状态是
f[0,j] = 0对于任意空间一个都不选就是无用功。 -
公式或者逻辑两个维度来看,i从1-n。
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
// 第0位,置为0,不参与计算,便于与后面的下标进行统一
int f[N][N];
// 动态规划
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= V; ++j) {
f[0][j] = 0;
if (j < v[i])
f[i][j] = f[i-1][j];
// 当前背包容量不够装第i个物品
else
// otherwise
f[i][j] = max(f[i-1][j],f[i-1][j-v[i]]+p[i]);
}
}
整数规划使用计算机求解好简单。 并且外层遍历使用元素选或者不选比较接近结果。就是V每一次只是加一进行遍历有点慢。但是也很必要,因为在其中的每一个数都可能包含一个结果。 总时间复杂度=状态数×得到每个状态的时间复杂度
滚动数组-两颗石头过河:
某些问题只需要保存两种状态因此,我们只用两行数据。 因为传递过程中只用到了前一位置的关系,那么我们如何标记现在是第几行也就是现在遍历到哪一个元素了呢?
-
还是遍历了n个元素但是给f[i]数组时只用了两行数组。以前的元素都可以清除掉。
-
而且由于是从0开始的,奇数仍然是奇数,1=1;
-
另一个问题判断奇偶性,可以使用
%2;>>i;或者位运算&1= &000000.....1 -
使用该方法由于改变了结果数组的存储方式因此最后取结果时还需要对i结果奇偶性判断后取的结果。
背包问题由于从0开始时,传递的初始状态为0,在此基础上。
优化到一维数组:
只需要对前一个数组的大于v[i]的容量部分更新就好了,可以一边遍历一边更新,因为i是依靠i-1的,V也是依靠V--的。并且已知前面的可以很容易推出后面的东西。
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]);
}
}
背包问题的分析方式是通过讨论每个物品“放与不放“,连接前i-1i−1个物品的状态和前ii个物品的状态之间的关系。 由解决了一个问题,不同的是价格计算方法不一样。
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
int w[35], v[35], f[50005]; // w代表价格,v代表重要程度
int n, m;
// n表示钱数,m表示个数
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++)
//从1开始
{
scanf("%d %d", &w[i], &v[i]); //输入每个商品的价格和重要程度
}
for (int i = 1; i <= m; i++)
{
for (int j = n; j >= w[i]; j--)
{
f[j] = max(f[j], f[j - w[i]] + v[i] * w[i]);
}
}
printf("%d", f[n]); //输出
return 0;
}