devc++ 技巧
#include <bits/stdc++.h>
基本包含所有头文件, 记不住的话,用这个, 但是 影响速度
std::ios::sync_with_stdio(false),std::cin.tie(0),std::cout.tie(0);
取消同步(节约时间,甚至能多骗点分,最好每个程序都写上)
return 0
必须用这个,蓝桥杯规定
int 21亿 超过21亿 开long long int
第一章 基础
1.string
string.substr(起始位置, 长度) 获取部分字符串 长度不要越界
string(个数, 字符) 重复字符的字符串
输入一行字符串 getline(cin,string类型变量
c_str() printf()里不能输出c++的string, 所以要用c_str(), 在 C 语言中,`printf` 函数确实可以用来输出字符串,但是这里的“字符串”指的是以 `'\0'`(空字符)结尾的字符数组(即 C 风格的字符串),而不是 C++ 中的 `std::string` 类型。
.length() .size()
+或者 append()
.find("") 返回子字符串起始位置
.replace( 起始位置, 长度, "要替换的" )
.conpare() 或者 直接 <, >
auto(引用,看修改不修改)枚举 遍历
std::string::length() 方法返回的是一个 std::string::size_type 类型的值,这是一个无符号整数类型,通常是 size_t。如果你想将这个值用于可能涉及负数的计算或者与有符号类型比较,那么进行类型转换是有好处的,以避免潜在的类型不匹配问题或整数溢出错误。
std::reverse
这是 C++ 标准库提供的一个函数,可以反转任何顺序容器(如 std::string, std::vector, std::deque 等)的元素。
std::string 的成员函数 rbegin() 和 rend()
如果你只是需要以反向的顺序遍历字符串(而不是实际修改字符串),可以使用反向迭代器
c++ 的 string, erase(index,1) 可以删除从 index 位置开始的 1 个字符
2.输入输出
%s, 遇到空格或者回车 会停下
cin 输入遇到空格或者回车也会结束
如果你想使用 `cout` 控制输出的小数点位数,可以使用 `iomanip` 库中的 `setprecision` 和 `fixed` 这两个操作符。这些操作符提供了一种灵活的方式来格式化浮点数输出。
使用 `std::fixed` 和 `std::setprecision` 时的区别主要在于对输出格式的影响,尤其是当输出浮点数时。下面,我将详细说明使用和不使用 `std::fixed` 时的不同行为:
使用 `std::fixed`
- 当你使用 `std::fixed` 时,`std::setprecision(n)` 设置的是小数点后的精确位数。这意味着无论数字的整数部分有多大,显示的小数位数都会固定为 `n` 位。
- `std::fixed` 使输出为固定点格式,而**不是科学记数法**(例如,不会显示为 1.23e+003)。
不使用 `std::fixed`
- 当不使用 `std::fixed` 时,`std::setprecision(n)` 设置的是数字的总有效数字位数(包括整数部分和小数部分)。这意味着,如果一个数字的整数部分很大,可能会没有小数部分显示。
- 不使用 `std::fixed` 可以让浮点数在必要时自动转为科学记数法格式,尤其是当数字非常大或非常小的时候。
std::cout.unsetf(std::ios_base::fixed); 必要的, 重置到非固定(浮动)格式,并设置总体精度为科学记数法显示
取消同步流
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
默认情况下,C++ 的输入输出库(iostream)与 C 的标准输入输出库(stdio)是同步的,以保证从 std::cin 读取的数据与从 scanf 读取的数据是一致的,以及 std::cout 和 printf 输出到相同的输出流中,且顺序相同。这个同步操作虽然保证了一致性,但也增加了性能开销。
取消这种同步可以在不需要同时使用 C 和 C++ 标准 I/O 库的情况下提高性能,特别是在涉及大量输入输出操作时。
3.竞赛常用函数
c++标准库很多都是左闭右开原则, .end()指向的是最后一个地址的下一位, .begin()是起始地址
1.大小写转换相关
islower
isupper
需要cctype头文件
tolower
toupper
以上函数只对单个字符有效!!!!!
ascii 数字,大小写字母
数字0,48开始
字母A 65开始
字母a 97开始
大小写 差 32
2.二分查找函数
仅对单调数组有效
binary_search(1,2,3) 返回bool binary:二进制的,二元的
lower_bound (起始地址,结束地址的下一位, 元素) 左闭右开
upper_bound
lower_bound 和 upper_bound 是 <algorithm> 库中的两个非常有用的函数,它们都用于在已排序的范围内进行二分查找,以帮助快速找到特定元素的位置。这些函数主要用于数组或容器,如 std::vector。 非降序有用
翻译成界限更好理解
3.排序
std:: sort函数 <algorithm> 库 使用快速排序 O(nlongn)
sort(起始地址,结束地址的下一位,*比较函数(默认小于号,这里就是自定义的函数了)) 左闭右开 <,<,<, 即默认升序
" \n"[i == n - 1] 部分:
" \n" 是一个包含两个字符的字符串字面量,其中包括一个空格 ' ' 和一个换行符 '\n'。
[i == n - 1] 是一个条件表达式,当 i 等于 n-1(也就是循环到最后一个元素时)时,表达式的值为 true,在 C++ 中 true 可以被隐式转换为整数 1。如果 i 不等于 n-1,则条件表达式的值为 false,等同于整数 0。
4.全排列(permutation)
next_permutation(起始地址,结束地址下一位)函数
prev_permutation()函数
返回 bool
5.最值
min
max
O(1)或者O(n) algorithm头文件
min_element max_element(起始地址,结束地址下一位) algorithm头文件 返回的是地址 O(n)
nth_element(起始地址, k, 结束地址下一位 ) O(n) 部分排序
6.其他常用库函数
在C语言中,有几个常用的函数,包括memset
、swap
(尽管C标准库中没有直接提供名为swap
的函数,但它是编程中常见的自定义函数)、以及reverse
(同样,C标准库中没有直接提供reverse
函数,但它是处理数组或字符串时常用的操作,可以通过自定义函数或标准库中的其他函数组合实现)。下面我将分别解释这些函数的作用和用法。
1. memset
函数
作用:memset
函数用于将内存块中的前n个字节设置为指定的值。它通常用于初始化内存区域,比如将数组的所有元素设置为0或某个特定的值。
原型:void *memset(void *s, int c, size_t n);
s
:指向要填充的内存块的指针。c
:要设置的值。注意,这个值被转换为unsigned char
类型,然后填充到内存中。n
:要填充的字节数。
返回值:memset
函数返回指向内存块起始位置的指针,但通常这个返回值不会被使用。
2. swap
函数
传入引用
3.reverse 函数
左闭右开 适用于各种容器
4.unique()
左闭右开 去除相邻重复元素 指向去重后有实际数据的范围的尾后地址 O(n) 不相邻会出问题, 最好用之前排序(nlogn)
4.stl
1.list
std::list 是一个序列容器,以双向链表的形式实现。std::list 允许常数时间的插入和删除操作,并能在任何位置进行这些操作。但由于其元素不是连续存储的,std::list 不支持快速随机访问,这意味着你不能像使用数组那样通过索引直接访问元素。
常见操作
插入和删除:在任何位置插入和删除元素都非常高效。
排序:std::list 提供了成员函数 sort(),可以直接在列表上进行排序,而不需要复制其元素到其他容器。
反转:std::list 也有 reverse() 成员函数,可以反转列表中元素的顺序。
#include <iostream>
#include <list>
int main() {
// 创建一个 std::list
std::list<int> myList = {2, 4, 6, 8};
// 添加元素
myList.push_front(0); // 在列表前面添加
myList.push_back(10); // 在列表后面添加
// 遍历列表并打印元素
std::cout << "List contains: ";
for (int num : myList) {
std::cout << num << " ";
}
std::cout << std::endl;
// 删除元素
myList.pop_front(); // 删除第一个元素
myList.pop_back(); // 删除最后一个元素
// 再次打印列表
std::cout << "After pop operations: ";
for (int num : myList) {
std::cout << num << " ";
}
std::cout << std::endl;
// 排序列表
myList.sort();
// 打印排序后的列表
std::cout << "Sorted list: ";
for (int num : myList) {
std::cout << num << " ";
}
std::cout << std::endl;
// 反转列表
myList.reverse();
// 打印反转后的列表
std::cout << "Reversed list: ";
for (int num : myList) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
容量和大小操作
empty() - 检查列表是否为空。
size() - 返回列表中的元素数量。
max_size() - 返回列表能够容纳的最大元素数量。
修改列表
push_back(const T& value) - 在列表的末尾添加一个元素。
push_front(const T& value) - 在列表的开始处添加一个元素。
pop_back() - 移除列表的最后一个元素。
pop_front() - 移除列表的第一个元素。
insert(iterator pos, const T& value) - 在指定位置之前插入一个或多个元素。
erase(iterator pos) - 移除指定位置的元素。
clear() - 移除列表中的所有元素。
resize(size_type count, T value = T()) - 改变列表中元素的数量,可指定新元素的值。
排序和操作
sort() - 对列表中的元素进行排序,默认使用元素类型的 < 操作符比较。
reverse() - 反转列表中元素的顺序。
merge(list& other) - 合并两个已排序的列表。
unique() - 移除列表中相邻重复元素,只保留一个。
remove(const T& value) - 移除列表中所有与指定值相等的元素。
splice(const_iterator pos, list& other, const_iterator it) - 将元素从另一个列表转移至此列表指定位置。
数据访问
front() - 访问列表的第一个元素。
back() - 访问列表的最后一个元素。
2.map
std::map
是一个关联容器,存储键值对(key-value pairs),每个键都是唯一的。它按照键的顺序自动排序,通常使用红黑树实现。
o(logn)
使用std::map
需要包含头文件:
#include <map>
std::map<int, std::string> myMap; // 创建一个空的map
myMap[1] = "apple"; // 插入元素
myMap[2] = "banana"; // 插入元素
- 插入元素
myMap.insert({3, "orange"});
- 访问元素
std::cout << myMap[1]; // 输出 "apple"
- 查找元素
auto it = myMap.find(2);
if (it != myMap.end()) {
std::cout << it->second; // 输出 "banana"
}
- 删除元素
myMap.erase(1); // 删除键为1的元素
可以使用迭代器遍历std::map
: 也算是重点!!!
for (const auto& pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
- 查找、插入和删除操作的平均时间复杂度为O(log n),这是因为底层使用了红黑树。
如果你想使用自定义类型作为键,必须重载小于运算符(operator<
):
cpp复制代码struct MyStruct {
int x;
bool operator<(const MyStruct& other) const {
return x < other.x;
}
};
std::map<MyStruct, std::string> customMap;
std::map
中的键是唯一的,插入相同键时,原有值会被覆盖。- 可以使用
std::multimap
来允许相同的键存在。
是std::map
常用操作函数的列表:
- **插入**
- `insert(const value_type& val)`:插入一个键值对(pair)元素。
- `emplace(Key&& k, T&& v)`:在map中直接构造元素。
- **访问**
- `operator[](const Key& k)`:通过键访问值,若键不存在则插入默认值。
- `at(const Key& k)`:通过键访问值,若键不存在则抛出异常。
- **查找**
- `find(const Key& k)`:返回指向指定键的迭代器,若未找到则返回`end()`。
- **删除**
- `erase(const Key& k)`:删除指定键的元素。
- `erase(iterator position)`:删除指定位置的元素。
- `erase(iterator first, iterator last)`:删除指定范围内的元素。
- **大小和容量**
- `size()`:返回元素个数。
- `empty()`:检查map是否为空。
- **清空**
- `clear()`:清空所有元素。
- **遍历**
- `begin()`:返回指向第一个元素的迭代器。
- `end()`:返回指向最后一个元素之后的迭代器。
- `rbegin()`:返回指向最后一个元素的逆向迭代器。
- `rend()`:返回指向第一个元素之前的逆向迭代器。
- **其他**
- `count(const Key& k)`:返回指定键的元素个数(对于`std::map`,最多为1)。 因为只能存储一个相同键的值
- `lower_bound(const Key& k)`:返回指向第一个不小于给定键的元素的迭代器。
- `upper_bound(const Key& k)`:返回指向第一个大于给定键的元素的迭代器。
3.multimap
# include <map>
std::multimap
是C+
+标准模板库中的一种关联容器,类似于std::map
,但允许相同的键出现多次
几乎用不到, 删除一个时, 要指定, 不然会删除 所有这个键
count(const Key& k):返回指定键的元素个数(对于
std::map`,最多为1)。 mutimap 就多了
//访问很重要
#include <map>
std::multimap<int, std::string> mp;
mp.insert(std::make_pair(1, "Apple"));
mp.insert({1, "Banana"});
mp.insert({2, "Cherry"});
auto it = mp.find(1); // find只返回重复的第一个
if (it != mp.end()) {
std::cout << it->second << std::endl; // 输出 "Apple"
}
auto range = mp.equal_range(1); // 重点
for (auto it = range.first; it != range.second; ++it) {
std::cout << it->second << " "; // 输出 "Apple Banana"
}
mp.erase(1); // 删除键为 1 的所有元素
auto it = mp.find(1);
if (it != mp.end()) {
mp.erase(it); // 删除键为 1 的第一个元素
}
auto range = mp.equal_range(1);
mp.erase(range.first, range.second); // 删除键为 1 的所有元素
int count = mp.count(1); // 返回键为 1 的元素个数
std::cout << "Count: " << count << std::endl; // 输出 "Count: 2"
for (auto it = mp.begin(); it != mp.end(); ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
for (const auto &pair : mp) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
if (mp.find(2) != mp.end()) {
std::cout << "Key 2 exists." << std::endl;
} else {
std::cout << "Key 2 does not exist." << std::endl;
}
mp.clear(); // 清空 multimap 中的所有元素
mp.emplace(3, "Date"); //emplace() 可以直接在容器中构造元素,避免不必要的拷贝:
auto it_low = mp.lower_bound(1);
auto it_up = mp.upper_bound(1);
for (auto it = it_low; it != it_up; ++it) {
std::cout << it->second << " "; // 输出所有键为 1 的元素
}
//lower_bound(key) 返回指向第一个不小于 key 的元素的迭代器。
//upper_bound(key) 返回指向第一个大于 key 的元素的迭代器。
equal_range() 返回一个范围,包含所有与键 1 相关联的元素。
4.unordered_map
std::unordered_map
是 C++11 引入的关联容器,与 std::map
类似,它也是存储键值对的容器,但它的底层实现是基于哈希表的,因此与 std::map
不同的是,它不保证键的顺序。
std::unordered_map
使用哈希表来实现,因此插入、删除、查找等操作的平均时间复杂度为 O(1),而 std::map
是 O(log n)。
键值对的顺序是无序的,元素不会按键的顺序排列
# include <unordered_map>
有极好的平均时间复杂度 和 极坏的最坏时间复杂度
5.pair
std::pair
是 C++ 标准库中的一种简单的模板类,用于存储两个相关的数据项。它可以将两个不同类型的值组合在一起,并作为一个整体来传递和操作
std::pair
主要用于将两个值绑定在一起,它通常与容器(如 map
和 unordered_map
)一起使用来存储键值对。
自带排序是升序
#include <utility>
template <typename T1, typename T2>
struct pair {
T1 first;
T2 second;
};
std::pair<int, std::string> p; // 默认构造
p.first = 1;
p.second = "apple";
std::pair<int, std::string> p(1, "apple"); // 直接初始化
auto p = std::make_pair(1, "apple"); // 自动推断类型
std::cout << p.first << ": " << p.second << std::endl; // 输出 1: apple
std::pair 支持关系运算符,可以直接比较两个 pair,比较时首先比较 first,若相等则比较 second:
std::swap(p1, p2); //交换
struct MyStruct {
int id;
std::string name;
};
std::pair<int, MyStruct> customPair(1, {101, "example"}); // 自定义
嵌套使用
#include <iostream>
#include <utility> // 包含pair头文件
int main() {
// 嵌套pair,外层pair的first是一个pair,second是int
std::pair<std::pair<int, std::string>, double> nestedPair;
// 为内部pair赋值
nestedPair.first.first = 1;
nestedPair.first.second = "apple";
// 为外部pair的second赋值
nestedPair.second = 9.99;
// 输出嵌套pair的内容
std::cout << "ID: " << nestedPair.first.first << std::endl;
std::cout << "Name: " << nestedPair.first.second << std::endl;
std::cout << "Price: " << nestedPair.second << std::endl;
return 0;
}
6.queue
std::queue
是 C++ 标准库中的一种容器适配器,用于实现先进先出(FIFO)的数据结构。它只允许在队列的前端访问元素,并在队列的后端插入元素。
std::queue
的底层实现通常使用 std::deque
,也可以使用 std::list
。
主要用于需要顺序处理的场景,比如任务调度、消息传递等。
#include <queue>
std::queue<int> myQueue; // 创建一个空的队列
myQueue.push(1); // 在队列尾部插入元素1
myQueue.push(2); // 插入元素2
myQueue.pop(); // 移除队列前端的元素
int frontElement = myQueue.front(); // 获取队列前端的元素
if (myQueue.empty()) {
std::cout << "Queue is empty" << std::endl; //是否为空
}
std::size_t size = myQueue.size(); // 返回队列中元素的个数
//遍历
while (!myQueue.empty()) {
std::cout << myQueue.front() << std::endl; // 访问前端元素
myQueue.pop(); // 移除前端元素
}
7.priority_queue
优先队列 考频高!!!
std::priority_queue
是 C++ 标准库中的一种容器适配器,提供优先级队列功能。与普通队列不同,std::priority_queue
以堆的形式存储元素,默认情况下,它是一个最大堆,即每次访问和弹出的都是队列中优先级最高的元素(最大值)。
std::priority_queue
默认是最大堆,最高优先级的元素在队列顶部。
可以通过自定义比较函数实现最小堆或其他排序方式。
#include <queue>
std::priority_queue<int> maxHeap; // 创建一个空的最大堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap; // 创建一个最小堆
maxHeap.push(10); // 插入元素10
maxHeap.push(5); // 插入元素5
maxHeap.push(20); // 插入元素20
int topElement = maxHeap.top(); // 获取最大元素,值为20
maxHeap.pop(); // 移除最大元素(20)
if (maxHeap.empty()) {
std::cout << "Priority queue is empty" << std::endl; //是否为空
}
std::size_t size = maxHeap.size(); // 获取队列中的元素个数
默认情况下,std::priority_queue 是一个最大堆。如果你想要实现最小堆(即优先级最高的是最小值),可以通过第三个模板参数指定比较器,如 std::greater<T>。
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
minHeap.push(10);
minHeap.push(5);
minHeap.push(20);
std::priority_queue 没有迭代器,因此不能像其他容器一样直接遍历。如果想遍历所有元素,可以通过不断调用 top() 和 pop() 来访问和删除元素。
除了 std::greater
,你还可以为 std::priority_queue
提供自定义比较函数,以控制元素的优先级。例如,假设有一个自定义结构体 Person
,你可以按年龄排序。
// 自定义比较函数
#include <iostream>
#include <queue>
#include <vector>
struct Person {
std::string name;
int age;
};
// 自定义比较函数(按年龄排序)
struct ComparePerson {
bool operator()(Person const& p1, Person const& p2) {
return p1.age > p2.age; // 最小堆,年龄小的优先
}
};
int main() {
std::priority_queue<Person, std::vector<Person>, ComparePerson> personQueue;
// 插入Person对象
personQueue.push({"Alice", 30});
personQueue.push({"Bob", 25});
personQueue.push({"Charlie", 35});
// 输出按年龄排序的Person
while (!personQueue.empty()) {
std::cout << personQueue.top().name << " - " << personQueue.top().age << std::endl;
personQueue.pop();
}
return 0;
}
比较函数
(1) 可以用仿函数,加重载()运算符, ()会被隐式调用
(2)自定义比较函数 用lambda,建立临时函数
8.std::greater
std::greater
是 C++ 标准库中的一个函数对象(又称仿函数),定义在头文件 <functional>
中。它的作用是对两个对象进行“大于”比较,通常用于改变 STL 容器中的排序顺序。默认情况下,std::priority_queue
和其他容器适配器(如 std::set
和 std::map
)是按从大到小的顺序排列元素。使用 std::greater
可以使容器按从小到大的顺序排列,从而实现最小堆等功能。
#include <functional>
std::greater 的作用就是改变容器的默认比较方式。 不管是大小排序 还是小大排序 都是改变原来默认的
9.deque
deque
(双端队列)是标准模板库(STL)中的一种容器类,位于 <deque>
头文件中。deque
与 vector
类似,可以随机访问,但其特点是可以高效地在头尾两端插入和删除元素。
双端操作:支持在两端快速插入和删除,复杂度为 O(1)。
随机访问:支持像数组一样的随机访问,复杂度为 O(1)。
动态扩展:与 vector
不同,deque
不需要单块连续内存,可以在两端动态分配内存,因此在两端插入时更高效。
#include <deque>
std::deque<int> d; // 创建一个空的双端队列
std::deque<int> d2(5, 10); // 创建一个包含 5 个元素,值均为 10 的双端队列
// 在尾部添加元素
d.push_back(1);
d.push_back(2);
// 在头部添加元素
d.push_front(0);
// 输出队列中的元素
for (int i : d) {
std::cout << i << " ";
}
std::cout << std::endl;
// 删除尾部的元素
d.pop_back();
// 删除头部的元素
d.pop_front();
// 输出队列中的元素
for (int i : d) {
std::cout << i << " ";
}
push_back():在尾部添加元素。
push_front():在头部添加元素。
pop_back():删除尾部的元素。
pop_front():删除头部的元素。
at(index) 或 [index]:访问指定位置的元素。
int x = d[0]; // 通过下标访问元素(没有边界检查)
int y = d.at(1); // 通过 at() 访问元素(有边界检查)
int front = d.front(); // 访问头部的元素
int back = d.back(); // 访问尾部的元素
bool empty = d.empty(); // 检查队列是否为空
size_t size = d.size(); // 返回元素个数
d.resize(10); // 调整队列大小为 10
d.insert(d.begin() + 1, 5); // 在第 1 个位置插入元素 5
d.erase(d.begin() + 1); // 删除第 1 个位置的元素
d.clear(); // 清空队列,移除所有元素
std::deque<int> d2 = {1, 2, 3};
d.swap(d2); // 交换 d 和 d2 的内容
10.set
set
是标准模板库(STL)中的一种关联容器,通常用于存储不重复的元素,并以自动排序的方式维护这些元素。set
的底层实现为平衡二叉搜索树(如红黑树),因此插入、删除和查找的时间复杂度为 O(log n)。set
保证元素的唯一性,且会按照排序顺序存储元素(默认升序).
#include <set>
std::set<int> s; // 创建一个空的 set
std::set<int> s2 = {1, 2, 3, 4}; // 使用初始化列表创建 set
s.insert(5); // 插入元素 5
s.insert(3); // 插入元素 3,如果元素已经存在,不会插入
s.erase(3); // 删除元素 3
s.erase(s.begin()); // 删除第一个元素(通过迭代器删除)
if (s.find(5) != s.end()) {
std::cout << "元素 5 存在" << std::endl; // 查找元素 5 是否存在
}
bool exists = s.count(5); // 返回 1 表示存在,0 表示不存在
//set 中无法像数组一样通过下标访问元素,但可以使用迭代器遍历
for (auto it = s.begin(); it != s.end(); ++it) {
std::cout << *it << " "; // 使用正向迭代器遍历 set 中的元素
}
std::cout << std::endl;
for (auto elem : s) {
std::cout << elem << " "; // 使用范围 for 循环遍历
}
std::cout << std::endl;
bool empty = s.empty(); // 检查 set 是否为空
size_t size = s.size(); // 返回 set 中元素的个数
s.clear(); // 清空所有元素
//范围操作
auto lower = s.lower_bound(2); // 返回第一个大于等于 2 的迭代器
auto upper = s.upper_bound(4); // 返回第一个大于 4 的迭代器
for (auto it = lower; it != upper; ++it) {
std::cout << *it << " "; // 输出范围内的元素
}
std::cout << std::endl;
11.equal_range 不常用
equal_range
返回一对迭代器,其中:
- 第一个迭代器指向第一个大于或等于给定值的元素(等同于
lower_bound
的结果)。 - 第二个迭代器指向第一个大于给定值的元素(等同于
upper_bound
的结果)。
auto range = s.equal_range(value);
12.multiset
multiset
是标准模板库(STL)中的一种关联容器,与 set
类似,但允许存储重复的元素。它适合用于需要按顺序存储多个相同值的场景,且会自动按排序顺序(默认升序)维护元素。
允许重复:multiset
可以存储相同的值多次,而 set
不允许重复。
有序性:元素在 multiset
中按照排序规则自动排序,默认为升序。
时间复杂度:插入、删除和查找操作的平均时间复杂度为 O(log n),因为底层实现是平衡二叉搜索树(如红黑树)。
13.unordered_set
无序
14.stack
stack
是标准模板库(STL)中的一种容器适配器,用于实现后进先出(LIFO, Last In First Out)的数据结构。这意味着最后插入的元素最先被取出。
stack
基于其它容器(例如 deque
,vector
或 list
)实现,只暴露了一组有限的操作,用于对栈进行元素的插入、访问和删除操作
#include <stack>
std::stack<int> s; // 创建一个空的栈
s.push(10); // 将元素 10 压入栈顶
s.push(20); // 将元素 20 压入栈顶
s.pop(); // 移除栈顶的元素
int top = s.top(); // 获取栈顶元素,不移除
bool isEmpty = s.empty(); // 返回 true 如果栈为空
size_t size = s.size(); // 返回栈中元素的个数
15.vector
vector
是标准模板库(STL)中一种非常常用的动态数组,可以根据需要自动扩展或缩小。它类似于普通数组,但具备了更多的灵活性和功能,例如动态调整大小、便捷的插入和删除操作。
动态扩展:vector
可以根据需要自动调整其大小,容纳更多元素。
随机访问:支持使用下标操作符 []
或 at()
进行随机访问,访问时间复杂度为 O(1)。
连续存储:vector
的内存是连续的,这使得它与普通数组兼容,适合与 C 风格的函数交互。
#include <vector>
std::vector<int> v; // 创建一个空的 vector
std::vector<int> v2(5); // 创建一个包含 5 个元素的 vector,元素默认初始化为 0
std::vector<int> v3(5, 10); // 创建一个包含 5 个元素的 vector,每个元素初始化为 10
std::vector<int> v4 = {1, 2, 3, 4, 5}; // 使用初始化列表创建 vector
v.push_back(10); // 在 vector 的尾部添加元素 10
v.insert(v.begin(), 5); // 在开头插入元素 5
v.pop_back(); // 移除 vector 尾部的元素
v.erase(v.begin()); // 删除第一个元素
v.clear(); // 清空所有元素
int first = v[0]; // 使用下标访问第一个元素
int second = v.at(1); // 使用 at() 方法访问第二个元素,有边界检查
int front = v.front(); // 获取第一个元素
int back = v.back(); // 获取最后一个元素
size_t size = v.size(); // 获取元素的个数
size_t capacity = v.capacity(); // 获取当前容量(可能大于 size)
bool isEmpty = v.empty(); // 检查 vector 是否为空
v.resize(10); // 调整 vector 的大小为 10 一般开始之前用
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " "; // 使用正向迭代器遍历元素
}
std::cout << std::endl;
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
std::cout << *rit << " "; // 使用反向迭代器遍历元素
}
std::cout << std::endl;
std::vector<int> v2 = {5, 6, 7};
v.swap(v2); // 交换 v 和 v2 的内容
16.size()
是无符号数,尽量不要出现size()-1, 不然就强转size()为int
17.at()
有边界检查,推荐这个
18.vector排序去重
vector
进行排序并去除重复元素,以获得一个不包含重复值的有序 vector
。以下是实现这一功能的步骤:
排序:使用 std::sort
对 vector
进行排序。
去重:使用 std::unique
去除相邻的重复元素。
调整大小:使用 erase
将 vector
调整为去重后的大小。
#include <iostream>
#include <vector>
#include <algorithm> // std::sort, std::unique
int main() {
// 创建一个包含重复元素的 vector
std::vector<int> v = {1, 3, 2, 3, 4, 1, 5, 4, 6};
// 对 vector 进行排序
std::sort(v.begin(), v.end()); // 排序后:1, 1, 2, 3, 3, 4, 4, 5, 6
// 使用 std::unique 去除相邻的重复元素
auto last = std::unique(v.begin(), v.end()); // [1, 2, 3, 4, 5, 6, _, _, _]
// 使用 erase 函数调整 vector 的大小
v.erase(last, v.end()); // 现在 v 为:1, 2, 3, 4, 5, 6
// last是去重后 实际数据的尾后地址 v.end()是原来v的尾后地址
// 输出去重后的 vector
std::cout << "排序并去重后的 vector: ";
for (int elem : v) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
第一章习题
1.宝藏排序(考察sort)
#include <iostream>
#include <algorithm>
int main()
{
std::ios::sync_with_stdio(false),std::cin.tie(0),std::cout.tie(0);
int n;
std::cin>>n;
int a[n];
for(int i=0;i<n;i++)
{
std::cin>>a[i];
}
std::sort(a,a+n);
for(int i=0;i<n;i++)
{
std::cout<<a[i]<<" \n"[i==n-1];
}
return 0;
}
//注意错点
int a[];是错的, 不能这个不指定大小 要么放在n被确定后 要么直接整个全局: const int N=1e5+9; int a[N];
2.吃糖果(优先队列)
这个题,先得理解题, 即 只要在排除最大的那类糖果数后, 剩余的可以插入到最大的那种糖果之间,就可以
#include <iostream>
#include <queue>
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n,x;
long long sum = 0;
std::priority_queue<int> sm;
std::cin>>n;
for(int i=0;i<n;i++)
{
std::cin>>x;
sm.push(x); // 默认大根堆 其实可以直接用 max()函数找最大
sum += x;
}
if(sum-sm.top() >= sm.top()-1)
{
std::cout <<"Yes";
}
else {
std::cout <<"No";
}
return 0;
}
//错误点: long long中间有空格, 注意数的范围,
3.括号匹配(考察栈)
空
4.快递分拣(map,vector,string综合)
map<string,vector> 表示 键值对 城市 : 单号
3
6661 北京
1616 上海
6161 北京
原始解法
#include <iostream>
#include <string>
#include <vector>
#include <map>
std::map<std::string, std::vector<std::string>> mp; //map不能重复,但是 值用vector存储,使得即使键重复了,但还是保证了一个键,多个值
std::vector<std::string> citys;
int main()
{
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
int n;
std::cin >> n;
std::string a, b;
for (int i = 0; i < n; i++)
{
std::cin >> a >> b;
if (!mp.count(b)) citys.push_back(b); //这里注意优先级, 不加(),会先执行!
mp[b].push_back(a); //mp[b]是值,即vector类型
}
for (std::string& city : citys)
{
// std::cout << city;
std::cout << city << " " << mp[city].size() << std::endl;
for(std::string &it : mp[city])
{
std::cout << it << std::endl;
}
}
return 0;
}
使用了不一样的解法,
#include <iostream>
#include <string>
#include <vector>
#include <map>
std::multimap<std::string, std::string> mp; //multimap 允许重复
std::vector<std::string> citys;
int main()
{
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
int n;
std::cin >> n;
std::string a, b;
for (int i = 0; i < n; i++)
{
std::cin >> a >> b;
mp.insert({ b,a });
// std::cout << mp.count(b)<<std::endl;
// if (mp.count(b)==1) citys.push_back(b);
if (!(mp.count(b)-1)) citys.push_back(b); //这里注意优先级, 不加(),会先执行!
}
for (std::string& city : citys)
{
// std::cout << city;
std::cout << city << " " << mp.count(city) << std::endl;
if (mp.count(city)>=1)
{
auto range = mp.equal_range(city);
for (auto it=range.first;it!=range.second;it++) // it返回的是pair类型.要用.first,.second访问
{
std::cout << (*it).second <<std::endl;
}
}
}
return 0;
}
第二章 算法基础
1.基础算法
1.时间复杂度
考试时,尽量控制在1e8以内
一般只卡时间,不卡空间
常见的时间复杂度包括:O(1)(常数时间,效率最高)、O(log n)(对数时间,例如二分查找)、O(n)(线性时间,遍历数组)、O(n log n)(线性对数时间,常见于排序算法如快速排序)、O(n²)(平方时间,常见于双重循环如冒泡排序)、O(2ⁿ)(指数时间,例如递归解决复杂问题)、以及O(n!)(阶乘时间,效率最低,通常用于排列组合类问题)。时间复杂度越低,算法的效率越高。
递归: 时间O(2ⁿ) 空间0(n)
一般,堆栈空间只给8MB,所以递归深度不能太深,不宜超过1e6层
2.枚举
这里不适用enum,而是自己写那种的
//1
#include <iostream>
bool f(int x)
{
while(x)
{
int y=x%10; // 个位
int m = x / 10; // 十位
if((y==2||y==0||y==9||y==1) || (m==2||m==9||m==1)) return true;
else return false;
}
}
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n,sum;
std::cin>>n;
for (int i=1;i<n+1;i++)
{
if(f(i)) sum+=i;
}
std::cout << sum <<std::endl;
return 0;
}
//2
#include <iostream>
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n,a,b,c;
std::cin>>n;
std::cin>>a>>b>>c;
int count=0;
for(int i=1;i<n+1;i++)
{
if(i%a!=0&&i%b!=0&&i%c!=0) count +=1;
}
std::cout << count << std::endl;
return 0;
}
//3
#include <iostream>
#include <map>
#include <vector>
const int N=1e3;
const int M=1e3;
int a[N][M];
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n,m;
std::map<int,std::vector<int>> mp;
std::cin>>n>>m;
for (int i=0;i<n;i++)
{
for (int j=0;j<m;j++)
{
std::cin>>a[i][j];
mp[a[i][j]].push_back(1);
}
}
for (const auto& pair : mp) // 这个pair是变量名,不是std::pair
{
if (pair.second.size() > (n*m)/2) std::cout << pair.first << std::endl;
}
}
3.模拟(还有题没做)
//扫雷 解法1,使用两个数组
#include <iostream>
const int N=1000;
const int M=1000;
int a[N][M],b[N][M];
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n,m;
std::cin>>n>>m;
int num=0;
for (int i=0; i<n; i++)
{
for (int j=0; j<m; j++)
{
std::cin>>a[i][j];
}
}
for (int i=0; i<n+2; i++)
{
for (int j=0; j<m+2; j++)
{
if (i==0||i==4||j==0||j==5)
{
b[i][j]=0;
}
else
{
b[i][j]=a[i-1][j-1];
}
}
}
for (int i=0; i<n+2; i++)
{
for (int j=0; j<m+2; j++)
{
// std::cout << b[i][j] << " \n"[j==m+1];
if (i>0&&j>0&&i<n+1&&j<m+1)
{
if (b[i][j]==1)
{
a[i-1][j-1]=9;
}
else
{
num = b[i][j]+b[i-1][j-1]+b[i-1][j]+b[i-1][j+1]+b[i][j-1]+b[i][j+1]+b[i+1][j-1]+b[i+1][j]+b[i+1][j+1];
a[i-1][j-1]=num;
}
}
}
}
for (int i=0; i<n; i++)
{
for (int j=0; j<m; j++)
{
std::cout << a[i][j] << " \n"[j==m-1];
}
}
return 0;
}
//扫雷 解法2
4.递归
将大问题分解成小问题
要有递归终止条件(出口),避免无限递归
特定情况,递归和循环可以转换
//斐波那契数列
int f(int x)
{
if(x<3&&x>0) return 1;
else if(x==0) return 0;
return (f(x-1)+f(x-2));
}
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n,num;
std::cin>>n;
if (n<0)
{
std::cout << "输入错误!必须输入大于等于0的数:"<<std::endl; std::cin>>n;
}
num = f(n);
std::cout << num << std::endl;
return 0;
}
//左边一半拼接 自己解法 由于这个题只是拼接, 所以还能优化,就是不在左边拼接,因为不是拼成自然数,而是看有没有符合要求得值
#include <iostream>
int f(int x,int n)
{
int sum;
if (x>0)
{
sum=n;
if (sum < 10 &&sum >0) sum = x*10+sum;
else if (sum < 100) sum =x*100 +sum;
else sum=x*1000+sum;
std::cout << sum <<std::endl;
count++;
sum = f(x-1,sum);
}
else
{
return 0;
}
return sum;
}
int main()
{
int n,m;
int count;
std::cin>>n;
for (int i=n/2;i>0;i--)
{
f(i,n);
}
std::cout << count << std::endl;
return 0;
}
5.进制转换
一个是乘权重
一个是取余
for (int i = 1; i <= st.length(); i++) {
x = x * 16 + a[i];
}
//记一下这个,这个完成了 累加权重乘法 使用了累积法
不太好理解,但确实做到了
6.前缀和
前几个数组值的和
利用前缀和,还可以快速得到 数组 某一区间的和
sum(l,r) = prefix[r]-prefix[l-1] l-r区间的和, 用r的前缀和 - l-1 的前缀和
适用于静态数组,
这个前缀和,先要自己循环,建立前缀和数组,再用
7.最大数组和
8.差分
即数组后一个减前一个
对差分数组使用前缀和, 可以还原原数组对应位置的值 ,相当于前缀和的应用
使用它,可以快速进行数组的区间修改!!!
不能实现边修改边查询, 只能多次修改,多次查询
边修改边查询需要 树状数组或者线段树等数据结构
有个题目,对数组进行了mci区间修改, 可以先计算差分, 然后进行m次区间修改, 最后使用前缀和 还原 修改后的数组
这个m次区间修改,即使区间不同,也可以重复进行,因为最终 差分 得到了叠加
9.离散化
把无限空间中的有限个体 映射到有限的空间中去
在C++中,离散化数组通常是将一个连续型数组的元素映射到一组有限的离散值(区间或分类)上。你可以使用自定义的方法根据等宽、等频等离散化策略,将数组中的值离散化。
一般离散化数组是去重的
10.贪心
贪心算法(Greedy Algorithm)是一种逐步构建解决方案的算法策略,在每一步都做出当前看起来最优的选择(局部最优解),希望通过局部最优解的选择最终得到全局最优解。贪心算法的特点是每一步都选择最优的局部解,而不回溯、不考虑其他可能的选择。
贪心选择性质:在求解的过程中,选择当前最优的局部解,且这些局部最优解可以累加形成最终的全局最优解。
无后效性:当前决策不影响未来的决策,即当前选择一旦做出,就不需要再考虑之前的状态,这也是贪心算法的一个关键性质。
贪心算法并不总是能找到全局最优解,但在一些特殊情况下,它是有效的。常见的可以使用贪心算法的场景包括:
- 活动选择问题:从一组活动中选择不重叠的活动,使得活动的数量最大化。
- 最小生成树问题:使用如 Kruskal 或 Prim 算法找到加权图的最小生成树。
- 哈夫曼编码:用于最优数据压缩的编码问题。
- 找零问题:在给定面值下,最少硬币找零。
- 区间调度问题:从若干个区间中选出互不重叠的区间数量最大化。
- 背包问题(部分情况下):在0-1背包问题中,贪心算法不适用,但在分数背包问题中,贪心算法是最优的。
11.二分
二分法(Binary Search)是一种在有序数组或列表中查找目标元素的算法。它通过每次将查找范围减半,从而大幅缩小查找的范围,因此效率很高。二分查找的时间复杂度为 O(logn)O(\log n)O(logn),适用于大规模数据集的高效查找。
在算法中,二分法 不仅可以用来查找数组中的元素,还可以用来求解各种数学问题。我们可以根据问题类型采用不同的二分策略,如整数二分、浮点二分和二分答案。
整数二分(Binary Search on Integers)是在一个有序的整数区间内进行二分查找的典型应用。它通常用于离散值上的问题,如查找某个整数、在整数范围内搜索满足条件的最优值等。
浮点二分(Binary Search on Floating Point Numbers)是对浮点数区间进行二分查找,通常用于逼近某个数值解。由于浮点数具有精度限制,因此在使用浮点二分时,需要给定一个误差范围(epsilon
)来控制查找的精度。
二分答案(Binary Search on Answer)是一种特殊的二分法应用,用于在一个解的范围上进行二分查找,找出满足某个条件的最优解。通常用于最优化问题,如求最小值或最大值。其思路是将可能的解作为二分查找的搜索空间,每次检查中间解是否满足条件。
整数二分:用于离散值问题,例如查找数组中某个值。
浮点二分:用于求解连续问题,例如求平方根、逼近某个浮点数解等。
二分答案:用于最优化问题,常与贪心法结合,求解解的空间。
二分答案的核心步骤:
check 是自定义的
-
设定可能的解的上下限
left
和right
。 -
通过二分查找,每次取
mid = (left + right) / 2
作为当前猜测的解。 -
使用条件判断函数
check(mid)
检查当前解是否满足要求:
- 如果
check(mid)
返回true
,表示当前解满足要求,则尝试更大的解(调整left
)。 - 如果
check(mid)
返回false
,则需要缩小范围(调整right
)。
- 如果
-
不断缩小范围,直到
left
和right
收敛到最优解。
二分答案经典例题 难理解 搞清楚一点, 二分的是 答案的区间,一个个试
https://majorli.github.io/algo_guide/ch02/sec04/248_p2678.html
跳石头
区间 [0,L]上的所有整数就是所有候选解。
#include <iostream>
const int N=100;
int a[N];
bool check(int mid,int m,int n) // 此时mid就是一个临时的最短跳跃距离
{
int remove=0,prev=0; // prev是移走的石头的前一个位置
for (int i=1;i<n+2;i++)
{
if(a[i]-a[prev]<mid)
{
remove++;
}
else
{
prev= i;
}
if (remove > m) return false; // 移除的石头超过限制,返回false
}
return true;
}
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int l,n,m;
std::cin>>l>>n>>m;
a[0]=0;
a[n+1]=l;
for (int i=1;i<n+1;i++)
{
std::cin>>a[i]; // a[i]是除了起点和终点 其余岩石距离起点的位置
}
int le=0,re=l,result=0; // 二分开始 , 0-l 之间的整数包含了本题的解, 所以要用这个 区间里的数, 使用二分查找,找符合条件的解
while(le<=re)
{
int mid = (le+re)/2;
if (check(mid,m,n))
{
result = mid; // 如果可以达到,则更新结果并继续尝试更大的跳跃距离 说明可以尝试继续变大,在右边的区间继续找
le = mid + 1;
} else {
re = mid - 1; // 否则尝试更小的跳跃距离
}
}
std::cout<<result<<std::endl;
return 0;
}
12.双指针(题)
双指针(Two Pointers)是一种常用的算法技巧,通常用于处理数组、链表等线性结构。通过两个指针(或标记)同时遍历来达到优化算法时间复杂度的目的。
对撞指针(又称为左右指针)是指将两个指针分别放在数组或链表的两端,向中间移动,以处理特定的问题。
适用场景:
- 排序数组中的问题:由于数组是有序的,通过对撞指针可以有效缩小范围,减少遍历次数。
- 寻找特定的元素组合:例如在一个有序数组中寻找两数之和为某个目标值。
- 判断回文串:通过两个指针分别从字符串的两端向中间逼近,逐个比较字符是否相同。
快慢指针通常用于链表或数组中,利用一个指针的移动速度比另一个快,从而获得特定的结论。
适用场景:
- 检测链表是否有环:通过让快指针每次走两步,慢指针每次走一步,如果链表有环,两个指针会相遇。
- 寻找链表的中间节点:快指针走两步,慢指针走一步,当快指针到达末尾时,慢指针正好位于链表的中间。
- 解决循环数组或序列问题:检测是否存在周期性或循环。
13.构造 (题)
构造算法一般是指通过一定的规则或策略,逐步构造出满足问题条件的解。这种方法往往适用于那些不容易直接通过搜索或枚举解决的问题,或者问题本身是通过某些步骤或过程构造的。
14.位运算
位运算是一种在二进制位上进行操作的算法技巧,通常用于提高程序的效率,特别是在处理整数或需要快速计算的场景下。位运算直接在二进制表示上进行操作,因此计算速度非常快,适合优化问题。
按位与 |
应用场景:用于掩码操作、判断奇偶性(x & 1
可以判断 x
是否为奇数)。
按位或 &
应用场景:可以用于设置某些位为 1
按位异或 ^
应用场景:用于交换两个数而不借助临时变量(a = a ^ b; b = a ^ b; a = a ^ b;
),还可用于判断两个数是否相等,异或为 0 时两个数相等。
按位取反 ~
应用场景:通常用于反转二进制位的值。
左移 <<
规则:将数字的二进制表示向左移动指定的位数,低位补 0
。
应用场景:用于快速乘以 2 的幂(如 x << 1
等价于 x * 2
,x << n
等价于 x * 2^n
)。
右移 >>
规则:将数字的二进制表示向右移动指定的位数,正数高位补 0
,负数高位补 1
(算术右移)。
应用场景:用于快速除以 2 的幂(如 x >> 1
等价于 x / 2
,x >> n
等价于 x / 2^n
)。
2.排序
常用的排序算法可以根据它们的时间复杂度、空间复杂度和稳定性进行分类和选择。以下是一些常用的排序算法及其特点:
1. 冒泡排序(Bubble Sort)
时间复杂度: O(n²)
空间复杂度: O(1)
稳定性: 稳定
特点:
通过重复地遍历数组,每次比较相邻的元素并交换,直到所有元素有序。
每一轮遍历后,最大的元素被“冒泡”到数组的最后位置。
适用场景: 简单场景下使用,适合小规模数据。
2. 选择排序(Selection Sort)
时间复杂度: O(n²)
空间复杂度: O(1)
稳定性: 不稳定
特点:
每次遍历时,选出未排序部分中的最小(或最大)元素,放到已排序部分的末尾。
不需要额外的存储空间,但不稳定。
适用场景: 适合数据较小且对空间要求较高的情况。
3. 插入排序(Insertion Sort)
时间复杂度: O(n²)
空间复杂度: O(1)
稳定性: 稳定
特点:
从第二个元素开始,将每个元素插入到前面已排好序的子数组中的适当位置。
适合数据规模较小或者部分已经排序的数据。
适用场景: 小规模或近似有序的数据。
4. 归并排序(Merge Sort)
时间复杂度: O(n log n)
空间复杂度: O(n)
稳定性: 稳定
特点:
基于分治法,将数组分为两部分,递归排序后合并。
即使在最坏情况下(完全逆序),时间复杂度也是 O(n log n)。
适用场景: 数据量大且需要稳定排序的场景。
5. 快速排序(Quick Sort)
时间复杂度: 平均 O(n log n),最坏 O(n²)
空间复杂度: O(log n)(递归栈空间)
稳定性: 不稳定
特点:
选择一个基准值(pivot),将数组分为小于基准值和大于基准值的两部分,递归地对两部分排序。
在随机分布的数据上表现优越,但在极端情况下(如已排序数据)会退化为 O(n²)。
适用场景: 常用的高效排序算法,适合大多数场景。
6. 堆排序(Heap Sort)
时间复杂度: O(n log n)
空间复杂度: O(1)
稳定性: 不稳定
特点:
基于堆数据结构(通常是最大堆或最小堆),通过将数组转化为堆并重复提取堆顶元素来排序。
不需要额外的存储空间。
适用场景: 适用于对空间有严格要求的场景。
7. 希尔排序(Shell Sort)
时间复杂度: 平均 O(n^1.3),最坏 O(n²)
空间复杂度: O(1)
稳定性: 不稳定
特点:
是插入排序的改进版,通过对数据分组进行插入排序,逐步减少间隔,最终对整个数组进行插入排序。
在中等规模的数据上比插入排序更快。
适用场景: 数据规模中等的场景。
8. 计数排序(Counting Sort)
时间复杂度: O(n + k),其中 k 是数值范围
空间复杂度: O(n + k)
稳定性: 稳定
特点:
非基于比较的排序算法,适用于数值范围较小的整数排序。
通过计数数组统计每个元素出现的次数,再根据计数将元素输出到结果数组。
适用场景: 适合范围有限的整数排序。
9. 基数排序(Radix Sort)
时间复杂度: O(d * (n + k)),其中 d 是数字的位数,k 是数值范围
空间复杂度: O(n + k)
稳定性: 稳定
特点:
将数据按位(或按某个数值范围)进行多次排序,通常使用计数排序作为子排序算法。
适用场景: 适用于数值范围大但位数较少的场景。
10. 桶排序(Bucket Sort)
时间复杂度: 平均 O(n + k),最坏 O(n²)
空间复杂度: O(n + k)
稳定性: 稳定
特点:
将数据分布到多个桶中,然后对每个桶内部进行排序,最后合并所有桶的数据。
适合数据分布均匀的场景。
适用场景: 适合分布较均匀的浮点数排序或有限范围的整数排序。
11. 冒泡排序改进版(如鸡尾酒排序)
时间复杂度: O(n²),但比普通冒泡排序更快。
特点:
双向遍历数组,将大的元素移动到末尾的同时,将小的元素移动到前面。
改善了冒泡排序在某些情况下的效率。
总结:
不同的排序算法适合不同的场景:
O(n²) 复杂度的算法(如冒泡排序、选择排序、插入排序)适合小规模数据或有特定要求的场景。
O(n log n) 复杂度的算法(如归并排序、快速排序、堆排序)适合大规模数据。
非基于比较的排序算法(如计数排序、基数排序、桶排序)在某些特定情况下表现非常优越,但适用范围有限。
1.冒泡
冒泡排序(Bubble Sort)是一种基础的排序算法,思想非常简单:通过重复地遍历待排序的数组,每次比较相邻的两个元素,如果它们的顺序错误(即前一个比后一个大),就交换它们的位置。这个过程持续进行,直到数组中没有任何元素需要交换为止。
#include <iostream>
const int N=1E8;
int a[N];
int main()
{
std::ios::sync_with_stdio(0),std::cout.tie(0),std::cin.tie(0);
int n;
std::cin>>n;
for(int i=0;i<n;i++)
{
std::cin>>a[i];
}
for (int i=0;i<n;i++)// 外层循环控制轮数 ,即待排序
{
for(int j=0;j<n-i-1;j++) // 内层循环控制 在剩余的里面排序
{
if(a[j]>a[j+1])
{
std::swap(a[j],a[j+1]);
}
}
}
for(int i=0;i<n;i++)
{
std::cout<<a[i];
}
return 0;
}
2.选择
选择排序(Selection Sort)是一种简单直观的排序算法。它的基本思想是:每次从待排序的数组中选择最小(或最大)元素,将其放到数组的起始位置,接着在剩余未排序的部分中继续寻找最小(或最大)元素,重复这个过程,直到所有元素都被排序好。
#include <iostream>
const int N=1e6;
int a[N];
void selectsort(int n)
{
int result;
for(int i=0;i<n;i++) // 5 2 4 6 3
{
result = i; // 关键点
for(int j=i;j<n-1;j++) //拿到待排序的最小值
{
if(a[result]>a[j+1])
{
result = j+1;
}
}
std::swap(a[i],a[result]);
}
}
int main()
{
std::ios::sync_with_stdio(0);std::cout.tie(0),std::cin.tie(0);
int n;
std::cin>>n;
for (int i=0;i<n;i++)
{
std::cin>>a[i];
}
selectsort(n);
for (int i=0;i<n;i++)
{
std::cout<<a[i];
}
return 0;
}
3.插入
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理类似于玩扑克牌时整理手牌:从左到右逐个取出元素,然后将其插入到已经排序好的部分的适当位置。它的主要思想是:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
#include <iostream>
const int N=1e8;
int a[N];
void insertsort(int n)
{
int val;
for (int i=1;i<n;i++) // 5 2 3 6 4 23564 未排序数组 23546
{
val=a[i];
int j=i-1; // 这个是为了确保最后再插入,而不是变比较边插入
for (;j>=0&&a[j]>val;j--) // 已排序数组
{
if(a[j]>val)
{
a[j+1]=a[j];
}
}
a[j+1] = val;
}
}
int main()
{
std::ios::sync_with_stdio(0),std::cout.tie(0);std::cin.tie(0);
int n;
std::cin>>n;
for (int i=0;i<n;i++)
{
std::cin>>a[i];
}
insertsort(n);
for (int i=0;i<n;i++)
{
std::cout<<a[i];
}
return 0;
}
4.快速
快速排序(Quicksort)是一种经典的分治排序算法。它的核心思想是:通过一个“基准元素”将待排序的数组划分为两个子数组,其中一部分所有元素小于基准元素,另一部分所有元素大于基准元素。然后分别递归地对这两个子数组进行排序。
注意:
在下面这个快排中, i++ 和 j–的先后顺序取决于 基准元素是开头的,还是结尾的
一定要遵循左右左右
即 若是开头(左),先j–(右), 若是结尾(右),先i++(左)
#include <iostream>
const int N=1e8;
int a[N];
//int select(int l,int r) // 以最左边为基准
//{
// int base=a[l];
// for(int i=r;i>=0;i--) // 这个结果正确的原因是 这里i>=0, 是的结果对了,但是不是正宗的快排
// {
// if(l<r)
// {
// if(base>a[i])
// {
// std::swap(a[l],a[i]);
// l++;
// }
// else
// {
// std::swap(a[r],a[i]);
// r--;
// }
// }
// else return l;
// }
//}
//
int select(int l,int r) //正宗快排,是先从右往左找到一个比基准元素小的元素,再从左往右找到一个比基准元素大的元素,交换它们,直到左右指针交错。
{
int i=l,j=r;
int base=a[l];
while(i<j)
{
while(i<j&&a[j]>=base) j--; // 右边出现小于基准的会停下
while(i<j&&a[i]<=base) i++; // 左边出现大于基准的会停下
if(i<j) std::swap(a[i],a[j]);
else std::swap(a[i],a[l]); // 此时i=j,就是基准的位置了 由于之前while判定是 a[i]<=base 所以开头的还是基准元素
}
return i;
// while(i<j)
// {
// while(i<j&&a[i]<=base) i++; // 左边出现大于基准的会停下
// while(i<j&&a[j]>=base) j--; // 右边出现小于基准的会停下
//
//
// if(i<j) std::swap(a[i],a[j]);
// else std::swap(a[i-1],a[l]); // 此时i=j,就是基准的位置了 由于之前while判定是 a[i]<=base 所以开头的还是基准元素
//
// }
// return i-1;
//
}
void quicksort(int l,int r)
{
if (l<r)
{
int mid=select(l,r); // 返回基准元素正确位置
quicksort(l,mid-1);
quicksort(mid+1,r);
}
}
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n,l,r;
std::cin>>n;
for(int i=0;i<n;i++)
{
std::cin>>a[i];
}
l=0,r=n-1;
quicksort(l,r);
for(int i=0;i<n;i++)
{
std::cout<<a[i];
}
return 0;
}
5.归并
归并排序(Merge Sort)是一种基于分治法(Divide and Conquer)**的递归排序算法,它将数组分为两半,分别对两部分进行排序,然后将排好序的两部分合并起来。归并排序的关键步骤是**递归分解和合并,它的时间复杂度是 O(nlogn)O(n \log n)O(nlogn),在最坏情况下仍然保持稳定高效。
//自己写的
#include <iostream>
const int N=1e7;
int a[N],b[N];
int Merge(int l,int mid,int r) //合并已排序主体
{
int re=mid+1,ll=l,rr=r;
for(int i=l;i<=r;i++)
{
if(l<=mid&&re<=r)
{
if(a[l]<a[re])
{
b[i]=a[l];
l++;
}
else
{
b[i]=a[re];
re++;
}
}
else if(l>mid) // 左边已完
{
b[i]=a[re];
re++;
}
else if(re>r) //右边已完
{
b[i]=a[l];
l++;
}
}
for(int i=ll;i<=rr;i++) // l和r 已经被改变了 不能这么复制
{
a[i]=b[i];
}
}
void MergeSort(int l,int r) // 归并递归主体
{
if(l<r)
{
int mid = l+(r-l)/2; //找每个部分的中间点
MergeSort(l,mid);
MergeSort(mid+1,r);
Merge(l,mid,r); // 合并排序好的部分
}
}
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0);
int n,l,r;
std::cin>>n;
for (int i=0;i<n;i++)
{
std::cin>>a[i];
}
l=0;r=n-1;
MergeSort(l,r);
for (int i=0;i<n;i++)
{
std::cout<<a[i]<<" ";
}
return 0;
}
6.桶
桶排序(Bucket Sort) 是一种基于分布的排序算法,适用于数据范围有限且分布较为均匀的情况。它的思想是将输入数据分到一定数量的桶中,然后分别对每个桶中的数据进行排序,最后将各个桶中的数据合并得到最终结果。
桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。
#include <iostream>
const int N=1e5;
const int bucket_max=100;
const int bucket_num=10; // 固定为10个桶
int a[N],bucket[bucket_num][bucket_max]; //
int Mid_num(int i,int l,int r)
{
int base=bucket[i][l];
while(l<r)
{
while(bucket[i][r]>base) r--;
while(bucket[i][l]<=base) l++;
if(l<r) std::swap(bucket[i][r],bucket[i][l]);
else bucket[i][l] = base;
}
return l;
}
void QuickSort(int i,int l,int r) // 快排
{
if(l<r)
{
int mid=Mid_num(i,l,r);
QuickSort(i,l,mid-1);
QuickSort(i,mid+1,r);
}
}
void BucketSort(int n) // 静态数组实现,可以使用二维数组
{
int bucketcount[bucket_num] = {0}; //统计每个桶内的元素数量
for (int i=0;i<n;i++)
{
if (bucketcount[a[i]/10]<bucket_max) //a[i]/10
{
bucket[a[i]/10][bucketcount[a[i]/10]++]=a[i]; //bucket[a[i]/10] 是一个起始地址
}
}
// 桶内使用快速排序
int pos=0;
for(int i=0;i<bucket_num;i++)
{
int l=0,r=bucketcount[i]-1; // 这里是i了,是桶内了
QuickSort(i,l,r); // bucket[a[i]/10][bucketcount[a[i]/10]] 每个桶已经有序
for (int k=0;k<bucketcount[i];k++)
{
a[pos++]=bucket[i][k]; // 注意 索引
}
}
}
int main()
{
std::ios::sync_with_stdio(0),std::cout.tie(0),std::cin.tie(0);
int n;
std::cin>>n;
for (int i=0;i<n;i++)
{
std::cin>>a[i];
}
BucketSort(n); //算法主体
for (int i=0;i<n;i++)
{
std::cout<<a[i]<<" ";
}
return 0;
}
第三章 搜索
1.DFS基础回溯
DFS(深度优先搜索)与回溯算法通常结合使用,特别是在解决一些组合问题时,比如全排列、组合、子集问题等。DFS是一种遍历或搜索树或图的算法,而回溯是一种解决问题的策略,试图通过递归地构建解空间树来寻找问题的所有解。
DFS 是一种沿着树或图的某一分支尽可能深地搜索,直到找到解或走到死胡同为止,然后回溯到上一个节点继续搜索其他未访问的分支。这种方式使得DFS适合于问题的解空间可以表示为树或图的问题。
#include <iostream>
const int N=10;
int ans,a[N][N];
void NQueens(int dep,int n)
{
if(dep==n) // 以 11 为终止条件, 比10 好, 第10行还要看有没有
{
ans++; // 总答案次数
return;
}
for (int i=0;i<n;i++) // dep行第几个位置放置 i 表示列
{
if(a[dep][i]) continue; // 若不能放,直接下一个位置 所以不需要else
// a[dep][i]++; //多余了,后续米字型就加了
for(int j=0;j<n;j++) a[j][i]++; // 米字型, 全部不能放入
for(int j=0;j<n;j++) a[dep][j]++;
for(int j=dep,k=i;j<n&&k<n;j++,k++) a[j][k]++;
for(int j=dep,k=i;j>=0&&k>=0;j--,k--) a[j][k]++;
for(int j=dep,k=i;j<n&&k>=0;j++,k--) a[j][k]++;
for(int j=dep,k=i;j>=0&&k<n;j--,k++) a[j][k]++;
NQueens(dep+1,n); // 如果 能一直放下去, 后续的就不会执行
//恢复之前的状态
for(int j=0;j<n;j++) a[j][i]--;
for(int j=0;j<n;j++) a[dep][j]--;
for(int j=dep,k=i;j<n&&k<n;j++,k++) a[j][k]--;
for(int j=dep,k=i;j>=0&&k>=0;j--,k--) a[j][k]--;
for(int j=dep,k=i;j<n&&k>=0;j++,k--) a[j][k]--;
for(int j=dep,k=i;j>=0&&k<n;j--,k++) a[j][k]--;
}
}
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n; // 棋盘和皇后的数量
std::cin>>n;
NQueens(0,n); // 从第一行开始放置
std::cout << ans ;
return 0;
}
2.回溯剪枝
剪枝 是在回溯算法中常用的一种优化技术,它的核心思想是在搜索过程中,尽早排除不可能的解,避免不必要的递归,从而提高算法效率。
在 N 皇后
问题中,剪枝的关键在于减少无效的搜索空间。当你尝试把皇后放在棋盘的某个位置时,如果该位置所在的列、主对角线或次对角线已经有皇后,显然这个位置是不合法的,继续递归就没有意义了,这时我们直接跳过该位置,称之为剪枝操作。
3.记忆化搜索
记忆化搜索 是一种通过 递归+缓存 来优化递归算法的技术,也可以理解为一种带有剪枝的递归算法。它的核心思想是在递归过程中,把已经计算过的结果保存下来,以避免重复计算,从而提高效率。
记忆化搜索通常用于解决那些具有重叠子问题的递归问题。典型的应用场景包括 动态规划 问题,像斐波那契数列、背包问题、最短路径问题等。
// 优化的斐波那契数列, 原题是结果对1e9+7 取模, 如果不取模,输入5000会数据溢出
// 加了备忘录,避免重复计算
#include <iostream>
#include <cstring>
const int N=1e8, p=1e9+7;
long long int dp[N];
int f(int n)
{
if(n<=2) return 1;
if(dp[n]!=-1) return dp[n];
return dp[n]=(f(n-1)+f(n-2))%p;
}
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
memset(dp,-1,sizeof(dp)); // 初始化为-1,
int n;
long long int sum;
std::cin>>n;
sum = f(n);
std::cout<<sum<<"\n";
std::cout<<sizeof(sum);
return 0;
}
第四章 动态规划
1.动态规划基础
1.线性DP
动态规划(Dynamic Programming,简称DP)是一种通过将复杂问题拆分为多个子问题,逐步解决这些子问题从而解决原问题的算法思想。它通常用于解决具有重叠子问题和最优子结构性质的问题。
动态规划的基本思想是 “记忆化” 和 “自底向上”。它通过将问题分解为子问题,存储子问题的解,避免重复计算,从而提高效率。动态规划常常用于解决最优化问题,如最短路径问题、背包问题等。
转移状态
特点 动态规划 贪心算法
思路 考虑所有可能的解,并存储中间结果 每一步选择局部最优,不考虑全局
子问题 有重叠子问题 每步只解决一个子问题
回溯 可以通过存储子问题的解来回溯决策路径 不可回溯,一次决策不能更改
适用性 更普遍,适用于重叠子问题和最优子结构 仅适用于贪心选择性问题
时间复杂度 较高(通常是O(n²)或O(n³)) 较低(通常是O(n log n)或O(n))
解的质量 保证全局最优解 不一定保证全局最优,可能是次优解
贪心法只考虑每一步是最优解, 在某些地方并不适用
// 1536 数字三角形
#include <iostream>
const int N=100;
int a[N][N],dp[N][N];
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n; // 三角形行数
std::cin>>n;
for(int i=0;i<n;i++)
{
for(int j=0;j<i+1;j++)
{
std::cin>>a[i][j];
}
}
for (int i=n-1;i>=0;i--)
{
for(int j=0;j<i+1;j++)
{
if(i==n-1)
{
dp[i][j]=a[i][j];
}
else
{
dp[i][j] = a[i][j]+ std::max(dp[i+1][j],dp[i+1][j+1]);
}
}
}
std::cout << dp[0][0];
return 0;
}
2.二维DP
摆花问题
3.最长公共子序列 LCS longest common subsequence
最长公共子序列(Longest Common Subsequence, LCS)是指在两个字符串中,找出它们的最长子序列,使得该子序列在两个字符串中保持相对顺序(但不需要是连续的)。LCS问题是一个经典的动态规划问题,广泛应用于文本比较、基因序列分析等领域。
注意: 最长公共子序列不需要是连续的
// 1189 维护长度和具体值
4.LIS
*最长递增子序列(LIS,Longest Increasing Subsequence)**是指在一个给定的序列中,找出一个最长的子序列,使得该子序列中的元素是严格递增的。这个问题广泛应用于许多领域,如股票价格分析、序列模式分析等。
// 2049 蓝桥勇士
2.0-1背包问题
1.0-1背包
0-1 背包问题 是经典的动态规划问题,常用于优化选择的组合,使得在有限容量的背包中放入具有重量和价值的物品,选择的物品总重量不能超过容量,并使得总价值最大化。
问题描述:
假设有 N
件物品,每件物品的重量为 w[i]
,价值为 v[i]
。现在有一个背包,它的容量为 W
。求在不超过背包容量 W
的前提下,能够装入背包的物品的最大总价值是多少。每件物品只能选一次,不能拆分,这就是 0-1 背包问题的限制。
动态规划解法:
我们可以使用动态规划来解决这个问题,通过定义 dp[i][j]
来表示前 i
件物品,背包容量为 j
时能够获得的最大价值。
2.完全背包
完全背包问题是背包问题的一种变体,与0-1背包问题的区别在于:在完全背包问题中,每件物品可以选择任意多次,而不是只能选择一次。问题的目标仍然是,在背包容量不超过给定上限的情况下,选择物品使得背包内的物品总价值最大。
问题描述:
给定 N
种物品,每种物品的重量为 w[i]
,价值为 v[i]
,可以选取任意多个同一种物品。背包的总容量为 W
,求在不超过容量的情况下,装入背包的物品的最大总价值。
3.多重背包
多重背包问题 是 0-1 背包和完全背包问题的扩展。它允许每件物品最多被选取一定的次数,而不是像 0-1 背包那样只能选择一次,或者像完全背包那样可以无限次选择。
问题描述:
给定 N
种物品,每件物品的重量为 w[i]
,价值为 v[i]
,并且每件物品最多可以选择 n[i]
次。背包的容量为 W
,求在不超过背包容量的前提下,能够装入背包的物品的最大总价值。
动态规划解法:
多重背包问题的核心思想与 0-1 背包和完全背包类似,关键在于如何处理物品的数量限制。
4.二维费用背包和分组背包
二维费用背包问题是在传统背包问题的基础上扩展的。与普通的 0-1 背包问题相比,这种问题引入了第二种资源约束。例如,除了物品的重量外,还可能有另一种资源限制(如体积、时间等)。目标是在不超过两种资源限制的前提下,选择物品使得总价值最大化。
问题描述:
给定 N
件物品,每件物品有重量 w[i]
和体积 v[i]
,以及价值 val[i]
,还有两个背包容量限制 W
和 V
,分别表示背包能够承受的最大重量和体积。目标是使得选取的物品在不超过这两个限制的情况下,总价值最大化。
动态规划解法:
定义 dp[i][j]
表示当前背包剩余重量为 i
、剩余体积为 j
时的最大价值。
5.单调队列优化多重背包
单调队列优化多重背包问题是一种针对多重背包问题的优化技术,通过结合单调队列的思想,可以在时间复杂度上进一步优化。相比传统的二进制优化方法,单调队列优化在某些情况下能更有效地处理状态转移,尤其是在背包容量较大时。
问题描述:
在多重背包问题中,每种物品有数量限制 n[i]
,每件物品有重量 w[i]
和价值 v[i]
,背包的总容量为 W
,目标是在不超过背包容量的前提下,选择物品使得背包中的总价值最大。
3.DP模型(先不看)
1.树形DP
1.自上而下树形
2.区间DP
3.状压DP
4.数位DP
第五章 字符串
1.KMP字符串匹配
KMP算法(Knuth-Morris-Pratt Algorithm) 是一种用于在字符串中查找模式子串的高效算法,时间复杂度为 O(n+m)O(n + m)O(n+m),其中 nnn 是主串的长度,mmm 是模式串的长度。KMP算法的关键在于利用已经匹配过的部分信息来避免重复比较,从而提升匹配效率。
KMP算法的基本思想:
KMP算法通过构建一个 部分匹配表(next数组) 来记录模式串中每个位置的最长相同前后缀长度。当在主串中匹配失败时,借助这个表跳过一些重复的匹配操作。
核心步骤:
- 构建 next 数组:对模式串构建 next 数组,记录每个位置的最长相同前缀和后缀的长度。
- 字符串匹配:利用主串和模式串进行比较,如果匹配失败,通过 next 数组调整模式串的位置,而不需要将主串指针回退。
2.字符串哈希(不懂)
字符串哈希(String Hashing)是一种高效的字符串比较方法,通过将字符串转换为一个或多个固定长度的哈希值,能够在常数时间内比较两个字符串是否相等。字符串哈希的主要应用场景包括:快速查找子串、字符串匹配、检测重复字符串等。
基本思想:
将一个字符串视为一个大整数,并使用某种哈希函数将其映射为一个固定长度的整数值。常见的哈希方法基于 多项式哈希,即将字符串中的每个字符看作是一个数字,并使用特定的基数和模数来计算该字符串的哈希值。
3.MANACHAR回文(不懂)
Manacher算法 是一种用于在字符串中查找最长回文子串的高效算法。与传统的中心扩展法相比,Manacher算法通过利用对称性,避免了大量不必要的计算,能够在 O(n)O(n)O(n) 的时间复杂度内找到最长回文子串。
算法背景:
在寻找回文子串时,最直接的方法是以每个字符为中心进行扩展,检查其是否是回文子串。这种方法的时间复杂度为 O(n2)O(n^2)O(n2),其中 nnn 是字符串的长度。而 Manacher 算法通过巧妙的优化,将时间复杂度降低到线性的 O(n)O(n)O(n)。
核心思想:
Manacher 算法的核心是通过在原字符串中插入特殊字符(如 #
),将奇数长度的回文和偶数长度的回文统一处理。例如,将字符串 "abcba"
转换为 "#a#b#c#b#a#"
, 这样就可以避免奇偶性问题,保证所有回文都是奇数长度。
具体步骤:
- 字符串预处理: 在字符串的每个字符之间插入特殊字符
#
,并在开头和结尾添加^
和$
,这样可以防止越界并统一奇偶回文的处理。 例如,对于字符串"abcba"
,经过预处理后变成"#a#b#c#b#a#"
, 长度变为奇数长度。 - 维护回文半径数组: 使用一个数组
P
来记录以每个字符为中心的最大回文半径长度。P[i]
表示以位置i
为中心的回文子串的半径(不包括当前位置)。 - 中心扩展与对称性优化: 在遍历过程中,维护一个回文的右边界
R
和其对应的中心C
。如果当前字符位于R
以内,利用对称性,P[i]
可以直接通过P[2*C - i]
计算得到,从而避免了从头开始计算。 - 更新最优解: 每次找到更长的回文子串时,更新回文的中心和右边界,同时记录最长回文子串的位置和长度。
在 Manacher算法 中,回文半径 是指以某个字符为中心的最长回文子串的半径长度。具体地,回文半径表示从该字符向两侧扩展的最长回文子串的长度(不包括该字符本身的左右两边界)。这是 Manacher 算法用于高效计算回文子串的一个关键概念。
回文半径的解释:
- 对于一个字符串
T
(经过插入特殊字符#
和边界字符处理过的字符串),我们用P[i]
来表示以T[i]
为中心的最长回文子串的半径。 - 半径
P[i]
实际上是左右可以扩展多少个字符的数量。如果P[i] = k
,则以T[i]
为中心的最长回文子串长度为2*k + 1
(即左扩k
,右扩k
,加上中心字符)。
举例说明:
假设处理后的字符串为 T = "^#a#b#c#b#a#$"
, 其原始字符串为 "abcba"
。
- 以
T[6] = "b"
为中心的最长回文子串是"#a#b#a#"
, 其回文半径为 3,因为可以向两侧扩展 3 个字符。 P[6] = 3
,意味着最长回文子串可以从T[6]
扩展到T[3]
到T[9]
,即原始字符串 `"
4.字典树(不懂)
字典树(Trie,又称为 前缀树)是一种树形数据结构,专门用于处理字符串集合。它常用于快速检索字符串,特别适合处理前缀相关的问题,比如词典中的单词查找、自动补全、前缀匹配等。
字典树的特点:
- 节点表示:每个节点表示一个字符。
- 路径表示单词:从根节点到某一节点的路径,表示一个字符串的前缀。
- 公共前缀的合并:具有相同前缀的字符串会共享相同的前缀路径。
- 快速查找:查找单词的时间复杂度是 O(m)O(m)O(m),其中 mmm 是单词的长度。相比于哈希表,Trie 能高效处理前缀查询。
字典树的基本操作:
- 插入:将一个字符串插入到字典树中。
- 查找:查找一个字符串是否在字典树中。
- 前缀查询:查找是否存在以某个前缀开头的单词。
字典树的结构:
字典树的每个节点都有多个子节点,每个子节点对应一个字符。典型的字典树中会有以下结构:
- 根节点:通常为空,不表示任何字符。
- 子节点:每个节点存储当前字符,并有若干子节点指向下一个字符。
- 结束标志:每个单词的结束位置通常会设置一个布尔值
isEnd
来标识该节点是否为某个单词的结束位置。
5.01 trie 树(不懂)
01 Trie树(也称为 二进制字典树)是一种特殊的 Trie 树,用于处理只包含二进制数(0 和 1)的数据。01 Trie 树可以用于处理一系列位串(例如整数的二进制表示),并且常用于解决一些二进制相关的问题,如最大异或值问题、最小异或值问题等。
01 Trie树的结构:
01 Trie树的结构与传统的 Trie 树类似,只不过它的每个节点最多有两个子节点:
- 左子节点表示二进制位
0
。 - 右子节点表示二进制位
1
。
通过将每个数字的二进制表示插入到 01 Trie 树中,可以方便地处理与二进制相关的查询操作。
主要应用:
- 最大异或值问题:在一组整数中,寻找两个整数使得它们的异或值最大。
- 最小异或值问题:在一组整数中,寻找两个整数使得它们的异或值最小。
- 区间 XOR 相关问题:处理一组数的前缀异或值等。
第六章 数论
1.数论基础
1.矩阵乘法
比较基础, 嵌套循环
2.整除
默认向下取整
修改向上取整 原来的x/y 改为 (x+y-1)/y 或者 (x-1)/(y+1)
3.同余
使用后面的逆元
2.GCD/LCM
GCD(Greatest Common Divisor)和 LCM(Least Common Multiple)分别是指 最大公约数 和 最小公倍数。这两个概念在整数运算中非常重要,特别是在简化分数、求解最小公倍数等问题中有广泛应用。
计算 GCD 的方法:
最常见的方法是使用 欧几里得算法(Euclidean Algorithm),它的核心思想是利用以下性质来迭代计算 GCD:
欧几里得辗转相除法
GCD(a,b)=GCD(b,a%b)
当 b = 0
时,a
就是 GCD。
计算 LCM 的方法:
最小公倍数和最大公约数的关系可以用以下公式表示:
LCM(a,b)=a×b / GCD(a,b)
这是因为 GCD 是两个数的共同因子,而 LCM 是两个数的共同倍数,
c++ 直接调用函数 __gcd(a,b) __lcm(a,b)
#include // 需要包含这个头文件,因为 __gcd 通常和 algorithm 头文件一起提供 非c++17 注意是两个下划线
在 C++ 中,自 C++17 开始,标准库提供了直接调用 GCD 和 LCM 的函数,分别为 std::gcd
和 std::lcm
,它们位于头文件 <numeric>
中。这样就不需要自己手动实现了。
// 手动实现欧几里得算法计算 GCD
int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
//简化gcd
int GCD(int a,int b)
{
return b==0?a:GCD(b,a%b);
}
// 手动计算 LCM
int lcm(int a, int b) {
return a * (b / gcd(a, b)); // 避免溢出
}
3.素数筛选
防溢出, i<=n/i 要有等于号
1.朴素的素数筛选
最简单的方法是判断一个数 n
是否能被 2 到 n-1
的任意数整除。如果 n
能被其中任意一个数整除,n
就不是素数,否则 n
是素数。
bool is_prime(int n) {
if (n <= 1) {
return false;
}
for (int i = 2; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
2.优化的朴素法
根据数学性质,若 n = a * b
,则至少有一个因子 a
或 b
小于等于 √n
。因此,我们只需要判断 n
能否被 2 到 √n
之间的数整除即可。这将时间复杂度从 O(n) 降低到 O(n)O(\sqrt{n})O(n)。
#include <cmath> // 包含 sqrt 函数
bool is_prime(int n) {
if (n <= 1) {
return false;
}
for (int i = 2; i * i <= n; i++) { // i * i <= n 改成 i<=n/i 防止溢出
if (n % i == 0) {
return false;
}
}
return true;
}
进一步优化可以通过先检查 n
是否为偶数(即 n % 2 == 0
),若 n
是偶数且不等于 2,那么 n
一定不是素数。之后,我们只需要检查奇数即可。
bool is_prime(int n) {
if (n <= 1) return false;
if (n == 2) return true; // 2 是唯一的偶数素数
if (n % 2 == 0) return false; // 排除偶数
for (int i = 3; i * i <= n; i += 2) { // 只检查奇数
if (n % i == 0) return false;
}
return true;
}
3.埃拉托斯特尼筛法
当需要对一系列较小的数进行素数判定时,埃拉托斯特尼筛法(Sieve of Eratosthenes) 是一种非常高效的方法,它能在 O(nloglogn)O(n \log \log n)O(nloglogn) 的时间内找出范围内的所有素数。
原理:
- 初始化一个布尔数组
is_prime
,其中所有元素都设为true
。 - 从 2 开始遍历,如果
is_prime[i]
为真,则将i
的倍数都标记为false
。 这一步是思想的重点 - 重复上述操作,直到遍历到
√n
为止。
#include <vector>
#include <iostream>
void sieve_of_eratosthenes(int n) {
std::vector<bool> is_prime(n + 1, true);
is_prime[0] = is_prime[1] = false; // 0 和 1 不是素数
for (int i = 2; i * i <= n; i++) {
if (is_prime[i]) {
for (int j = i * i; j <= n; j += i) { // 倍数全部定为 false
is_prime[j] = false;
}
}
}
// 打印所有的素数
for (int i = 2; i <= n; i++) {
if (is_prime[i]) {
std::cout << i << " ";
}
}
}
int main() {
int n = 50;
std::cout << "小于等于 " << n << " 的所有素数: ";
sieve_of_eratosthenes(n);
return 0;
}
4.欧拉筛–没找到
5.唯一分解定理
唯一分解定理(Fundamental Theorem of Arithmetic),又称为算术基本定理,是数论中的一个重要定理。它指出:
每个大于 1 的正整数都可以唯一地分解为一个或多个质数的乘积,质数的顺序无关紧要。
换句话说,任何一个大于 1 的整数都可以表示为若干个质数的乘积,并且这种表示在不考虑乘积顺序的情况下是唯一的。
定理说明
-
存在性:任意大于 1 的正整数 n,都可以表示为有限多个质数的乘积,即:
n = p1^e1 * p2^e2 * ... * pk^ek
其中,
p1, p2, ..., pk
是质数,且e1, e2, ..., ek
是正整数。 -
唯一性:这种质数分解是唯一的(忽略质数的排列顺序)。也就是说,不可能存在两种不同的质数分解方式。
// 验证及实现
#include <iostream>
#include <vector>
using namespace std;
// 函数:求某个整数的质因数分解
vector<pair<int, int>> primeFactorization(int n) {
vector<pair<int, int>> factors; // 存储质因数及其对应的幂次
for (int i = 2; i * i <= n; ++i) { // 从 2 开始试除
if (n % i == 0) {
int count = 0;
while (n % i == 0) {
n /= i; // 这里变化的 n
count++; // 计算当前质数的幂次
}
factors.push_back({i, count}); // 记录质因数及其幂次
}
}
if (n > 1) { // 如果 n 本身是一个质数 当 n 在循环结束时仍然大于 1,它本身就是一个质数。
factors.push_back({n, 1});
}
return factors;
}
int main() {
int n;
cout << "输入一个正整数: ";
cin >> n;
// 处理质因数分解
vector<pair<int, int>> factors = primeFactorization(n);
// 输出分解结果
cout << n << " 的质因数分解结果为: ";
for (size_t i = 0; i < factors.size(); ++i) {
cout << factors[i].first << "^" << factors[i].second;
if (i != factors.size() - 1) {
cout << " * ";
}
}
cout << endl;
return 0;
}
6.约束个数定理–基于唯一分解定理
设 n
是一个正整数,若 n
的质因数分解为:
n = p1^e1 * p2^e2 * … * pk^ek
其中,p1, p2, ..., pk
是 n
的不同质因数,e1, e2, ..., ek
是对应的质因数的幂次。
则 n
的正约数个数 d(n)
为:
d(n) = (e1 + 1) * (e2 + 1) * … * (ek + 1)
例子:
12 = 2^2 * 3^1
d(12) = (2 + 1) * (1 + 1) = 3 * 2 = 6
12 的正约数为:1, 2, 3, 4, 6, 12。
2^0, 2^1, 2^2, 3^0, 3^1, 以及互乘的结果, 将是 12 的约数
// 100! 正约束个数
#include <iostream>
const int N=1e5;
int a[N];
void f(int n)
{
for (int i=2;i<=n/i;i++) // 特别注意, 质数,这个i<=sum/i 要有等于呀
{
if (n%i) continue;
while (n%i==0)
{
n/=i;
a[i]++;
}
}
if(n>1) a[n]++;
}
int main()
{
std::ios::sync_with_stdio(0),std::cin.tie(0),std::cout.tie(0);
int n=100;
for(int i=1;i<=n;i++) f(i);
long long int ans=1;
for(int i=1;i<=n;i++)
{
ans*=(a[i]+1);
}
std::cout << ans;
return 0;
}
7.快速幂(背,不好理解)
快速幂(Exponentiation by Squaring)是一种高效计算大数幂次的算法,特别是在计算模数(如模运算)时经常使用。它通过将幂次拆分为更小的幂次,通过乘方和平方的结合来减少乘法的次数,从而降低计算复杂度。
给定一个数 a
和一个正整数 b
,我们想计算 a
的 b
次幂,即 a^b
。快速幂算法的思想是通过逐次平方法来优化乘法次数,具体分为以下两种情况:
当 b
是偶数时:
a^b = (a(b/2))2
也就是说,a
的 b
次幂可以先计算 a
的 b/2
次幂,然后将其平方。
当 b
是奇数时:
a^b = a * a^(b-1)
当 b = 0
时:
a^0 = 1
示例
计算 3^13:
13 是奇数,所以将其转化为:
3^13 = 3 * 3^12
接下来计算 3^12,因为 12 是偶数:
3^12 = (3^6)^2
计算 3^6,因为 6 是偶数:
3^6 = (3^3)^2
计算 3^3,因为 3 是奇数:
3^3 = 3 * 3^2
计算 3^2,因为 2 是偶数:
3^2 = (3^1)^2
计算 3^1,因为 1 是奇数:
3^1 = 3
最终通过将结果逐步回推,可以得到 3^13 = 1594323。
复杂度分析
时间复杂度:快速幂算法的时间复杂度为 O(log b),因为每次将 b 减半。
空间复杂度:如果使用递归实现,空间复杂度为 O(log b),如果使用循环实现,空间复杂度为 O(1)。
#include <iostream>
// 快速幂函数,计算 a^b % mod
long long fast_pow(long long a, long long b, long long mod) {
long long result = 1;
while (b > 0) {
if (b % 2 == 1) { // 如果 b 是奇数
result = result * a % mod;
}
a = a * a % mod; // 平方底数
b /= 2; // 幂次减半
}
return result;
}
int main() {
long long a, b, mod;
std::cout << "请输入底数 a, 幂次 b 和模数 mod: ";
std::cin >> a >> b >> mod;
std::cout << "a^b % mod = " << fast_pow(a, b, mod) << std::endl;
return 0;
}
判断奇数技巧:
b&1; 按位与
将b与00000...001 按位与, 若b是奇数, 最低位一定是1, 则按位与 为 1
按位与要求, 必须 都为1 才是1
除以2 技巧:
b >>= 1 是一种右移运算,用于将变量 b 的二进制位右移一位。这意味着 b 的每一位都会向右移动,原本的最低位被丢弃,而最高位通常根据符号填充(对于无符号整数,填充 0)。
具体含义:
b >>= 1 等价于 b = b >> 1,将 b 的二进制表示向右移动一位。
移动一位相当于将 b 除以 2,并舍弃余数(取整)。
其余技巧:
在 C++ 编程中,除了 b >>= 1 这样的右移操作,其他位运算也常用于提升算法的效率,特别是在某些特定的场景下。下面列出了一些常见的位运算技巧及其用途:
1. b <<= 1 (左移一位)
与右移相反,左移一位相当于将数值乘以 2。
表示:b <<= 1 相当于 b = b * 2。
例子:
b = 3,二进制 011,b <<= 1 后变为 110,即 b = 6。
用途:
左移操作常用于快速计算乘 2 的幂,例如可以通过左移 n 次来实现乘以
2
𝑛
2
n
。
2. b & (b - 1) (消除最低位的 1)
这个操作可以将 b 的二进制表示中的最低位的 1 清除,常用于快速判断和处理位级信息。
表示:b & (b - 1) 清除 b 中最低的 1。
例子:
b = 6,二进制 110,b & (b - 1) 得到 100,即 b = 4。
b = 12,二进制 1100,b & (b - 1) 得到 1000,即 b = 8。
用途:
这个技巧常用于快速统计二进制数中 1 的个数(也就是位计数问题),比如在 Hamming Weight(汉明重量)算法中。
3. b | (1 << k) (将第 k 位设置为 1)
通过左移加上按位或操作,将第 k 位设为 1。
表示:b | (1 << k) 将 b 的二进制表示的第 k 位(从 0 开始计数)设置为 1。
例子:
b = 5,二进制 101,b | (1 << 1) 后,结果为 111,即 b = 7。
用途:
用于设置二进制数的某个位为 1。
4. b & (1 << k) (检查第 k 位是否为 1)
通过左移并按位与操作,检查第 k 位是否为 1。
表示:b & (1 << k) 用来检测 b 的第 k 位(从 0 开始计数)是否为 1。
例子:
b = 5,二进制 101,检查第 1 位(即 1 << 1,二进制 010)是否为 1,结果为 0,说明第 1 位是 0。
b = 6,二进制 110,检查第 1 位是否为 1,结果为非 0,说明第 1 位是 1。
用途:
用于快速判断二进制数某个特定位是否被设置为 1,在位操作或掩码中很常用。
5. b ^ (1 << k) (翻转第 k 位)
通过左移加异或操作,翻转第 k 位,即如果第 k 位为 1 则变成 0,若为 0 则变成 1。
表示:b ^ (1 << k) 翻转 b 的第 k 位。
例子:
b = 5,二进制 101,b ^ (1 << 1) 结果为 111,即 b = 7(第 1 位从 0 变为 1)。
再次执行 b ^ (1 << 1),结果变回 101,即 b = 5(第 1 位从 1 变回 0)。
用途:
用于翻转某个特定位,常用于状态切换或二进制位操作中。
6. b & (-b) (提取最低位的 1)
这个操作可以提取出 b 的二进制表示中最低位的 1。
表示:b & (-b),其中 -b 是 b 的二进制补码形式。
例子:
b = 12,二进制 1100,b & (-b) 得到 100,即 b = 4(提取最低位的 1)。
用途:
这个操作常用于快速定位或提取最低的 1 位,可以应用于集合中的位表示法。
7. b == (b & -b) (判断 b 是否是 2 的幂)
通过位运算,可以快速判断一个数 b 是否是 2 的幂次。
表示:如果 b == (b & -b),则 b 是 2 的幂。
例子:
如果 b = 4,二进制 100,b & -b = 4,所以 b 是 2 的幂。
如果 b = 5,二进制 101,b & -b = 1,所以 b 不是 2 的幂。
用途:
判断一个数是否为 2 的幂次,在某些算法中很有用,比如平衡二叉树的高度计算等。
8.费马小定理与逆元
费马小定理(Fermat’s Little Theorem)是数论中的一个重要定理,定理内容为:
费马小定理: 若 p
是一个素数,且 a
是一个不被 p
整除的整数(即 a
和 p
互质),则有: a^(p-1) ≡ 1 (mod p)
即 a
的 p-1
次幂模 p
的结果等于 1。
推广形式:
费马小定理的另一个常见形式是: a^p ≡ a (mod p)
费马小定理的应用:
- 求模逆元: 在模
p
的运算中,若我们要求a
在模p
意义下的逆元,利用费马小定理,可以得到: a^(-1) ≡ a^(p-2) (mod p) 这样可以快速求出a
的模逆元,时间复杂度为 O(log p)。 这里就用到了 快速幂 - 大幂取模的简化: 在计算
a^b (mod p)
时,若b
很大,可以先将b
对p-1
取模,以减少幂的大小,因为根据费马小定理,a^(p-1) ≡ 1 (mod p)。
逆元
逆元的定义:
对于给定的整数 a
和模数 p
,如果存在一个整数 x
使得
a * x ≡ 1 (mod p)
那么 x
被称为 a
在模 p
意义下的乘法逆元,或简称为逆元。
逆元的条件:
- 如果
a
和p
互质(即gcd(a, p) = 1
),则a
在模p
下有一个唯一的逆元。 - 如果
a
和p
不互质,则a
在模p
下不存在逆元。
逆元的应用:
- 求解同余方程:逆元可以用来解形如
a * x ≡ b (mod p)
的同余方程。 - 模除法运算:在模
p
意义下的除法可以转化为乘法逆元运算。即
a / b (mod p)
可以转化为
a * b^(-1) (mod p)
其中b^(-1)
是b
在模p
下的逆元。
逆元的求法:
- 费马小定理:
如果p
是素数,费马小定理告诉我们
a^(p-1) ≡ 1 (mod p)
因此可以得到
a^(-1) ≡ a^(p-2) (mod p)
这意味着a
的逆元可以通过计算a
的p-2
次幂模p
得到。 - 扩展欧几里得算法:
对于任意整数p
,可以使用扩展欧几里得算法来求解a * x + p * y = 1
,其中x
是我们最终需要的值,它是a
在模p
意义下的逆元。y是另一个解,只是在求解过程中起辅助作用,它并不影响
x` 的结果。
因此,y
的具体值不重要,关键是找到 x
,这个 x
就是 a
的逆元。
一般, 费马小定理结合逆元, 又会用到快速幂 三者结合
9.综合 费马小定理, 逆元, 快速幂
1. 费马小定理:
费马小定理是数论中的一个重要定理,它的表述如下:
如果 p
是一个素数,且 a
是一个与 p
互质的整数(即 gcd(a, p) = 1
),则有:
css
复制代码
a^(p-1) ≡ 1 (mod p)
这意味着 a
的 p-1
次幂在模 p
意义下等于 1。
2. 逆元:
在模 p
运算中,逆元是指这样一个数 x
,它满足:
css
复制代码
a * x ≡ 1 (mod p)
其中 a
和 p
互质。也就是说,x
是 a
的模 p
意义下的乘法逆元。
3. 逆元的求法有两种主要方法:
(1) 使用费马小定理:
根据费马小定理,如果 p
是素数,那么 a^(p-1) ≡ 1 (mod p)
,可以得到:
css
复制代码
a^(p-2) ≡ a^(-1) (mod p) 这个式子换个形式 a * a^(p-2) = 1 (mod p) 在逆元的公式里, x = a^(p-2), 即为 逆元
这意味着 a
在模 p
下的逆元就是 a^(p-2)
。
(2) 使用扩展欧几里得算法:
如果 a
和 p
互质,可以通过扩展欧几里得算法求解方程:
css
复制代码
a * x + p * y = 1
其中 x
就是 a
的逆元。
4. 快速幂:
快速幂是一种用于高效计算大数幂的算法。它的思想是通过“二分法”递归地减少计算次数,将幂的复杂度从 O(n) 降低到 O(log n)。
例如,计算 a^b (mod p)
时,如果 b
很大,快速幂可以将 b
拆分为更小的部分来快速计算。
关系总结:
-
费马小定理给出了
a^(p-1) ≡ 1 (mod p)
,利用它可以快速计算逆元:css 复制代码 a^(-1) ≡ a^(p-2) (mod p)
所以,当
p
是素数时,逆元可以通过费马小定理来求解。 -
逆元是指在模运算中与
a
相乘等于 1 的数,求逆元可以通过费马小定理或扩展欧几里得算法。 -
快速幂可以高效计算幂次模运算,在用费马小定理求逆元时,计算
a^(p-2) (mod p)
就可以通过快速幂来实现,从而提高效率。
实际步骤:
- 如果
p
是素数且需要求a
在模p
意义下的逆元,先通过费马小定理得知a^(-1) ≡ a^(p-2) (mod p)
,然后通过快速幂来计算a^(p-2) mod p
。 - 如果
p
不是素数,或者想用通用方法求逆元,则可以使用扩展欧几里得算法来求解。
这样,费马小定理、逆元和快速幂之间的关系就很清晰了。
5.总结
总结下来,如果 a
和 p
互质,且 p
是素数,根据费马小定理,a^(p-2)
就是 a
在模 p
意义下的逆元。这是因为根据费马小定理:
a^(p-1) ≡ 1 (mod p)
两边同时乘以 a^(-1)
(即逆元),我们就可以得到:
a^(-1) ≡ a^(p-2) (mod p)
因此,如果 a
和 p
互质,a^(p-2)
就是 a
的逆元。这种方法特别适用于 p
是素数的情况,计算时通常用快速幂来提高效率。
6.互质:
gcd(a,b)=1 并不是两个都是质数
10.欧拉降幂,欧拉函数(也是刘汝佳数论第一节)
欧拉函数 ϕ(n)的定义为:小于等于 n 的正整数中,与 n 互质的整数的个数。 这是定义,注意
注意,如果本身是质数, 则可以用第一种方式 计算 很快
单点欧拉函数求解
基于唯一分解定理, 得到质因子, 再利用欧拉公式得到
// 计算欧拉函数 φ(n)
int eulerPhi(int n) {
int result = n;
for (int i = 2; i <= n / i; ++i) {
if (n % i == 0) {
result = result / i * (i - 1); // 简化计算 直接计算了结果
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
result = result / n * (n - 1); // 处理剩余的质数
}
return result;
}
欧拉降幂
欧拉降幂用于计算形如 a^b mod m
的问题。其核心思想是通过欧拉定理来简化指数的计算。欧拉定理告诉我们,当 a
和 m
互质时,可以通过欧拉函数 φ(m)来减少计算的大数幂。
前置是 欧拉定理
欧拉定理
如果正整数 a 和 n 互质(即 gcd(a, n) = 1),那么有:
a^φ(n) ≡ 1 (mod n)
φ(n) 是欧拉函数,表示小于或等于 n 的与 n 互质的数的个数。
费马小定理(Fermat’s Little Theorem)是欧拉定理的特例,当 n 是质数时使用。它描述了在模质数运算中的一个简化情况。具体内容为:如果 p 是质数,且正整数 a 和 p 互质(即 gcd(a, p) = 1),那么有:
a^(p-1) ≡ 1 (mod p)
重点:: 费马小定理是欧拉定理的一个特例。对于质数 p,φ§ = p - 1,因此费马小定理可以看作是欧拉定理在 n 为质数时的特殊情况。
欧拉降幂
如果我们需要计算 a^k (mod n),且 k 很大,我们可以使用欧拉降幂将指数 k 降到一个更小的值。具体步骤如下:
当 k ≥ φ(n) 时,先计算 k mod φ(n),得到余数 r,然后有:
a^k ≡ a^r (mod n)
其中 r = k mod φ(n)。
当 k < φ(n) 时,直接计算 a^k (mod n) 即可。
对唯一分解定理, 费马小定理, 欧拉函数, 快速幂 的综合应用
质数, 在题中, 必须一眼判定出来
欧拉降幂快速做题(代码不连, 记住怎么写)
//对于 a^b (mod c)
// 化为 a^b ≡ a^(b mod φ(c)) (mod c)
// 欧拉函数的主体 先计算 φ(c)
int eulerPhi(int n) {
int result = n;
for (int i = 2; i <= n / i; ++i) {
if (n % i == 0) {
result = result / i * (i - 1); // 简化计算 直接计算了结果
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
result = result / n * (n - 1); // 处理剩余的质数
}
return result;
}
// 得到 b mod φ(c)
// 快速幂主体 对降幂后的进行快速幂计算
long long fast_pow(long long a, long long b, long long mod) {
long long result = 1;
while (b > 0) {
if (b % 2 == 1) { // 如果 b 是奇数
result = result * a % mod;
}
a = a * a % mod; // 平方底数
b /= 2; // 幂次减半
}
return result;
}
m! mod c 为了避免m过大, 可以地推进行 i mod c i从1-m
10.exgcd与裴属定理
裴蜀定理表述为:对于任意两个整数 a 和 b,它们的最大公约数 gcd(a, b) 可以表示为这两个整数的线性组合,即存在整数 x 和 y,使得:
ax + by = gcd(a, b)
这意味着,两个整数的最大公约数可以通过这两个数的整数系数的线性组合表示。这个系数 x 和 y 的值可以通过扩展欧几里得算法求得。
#include <iostream>
using namespace std;
// 扩展欧几里得算法,计算 gcd(a, b),同时找到 x 和 y 使得 ax + by = gcd(a, b)
int extendedGCD(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1;
y = 0;
return a; // 返回 gcd(a, 0) = a
}
int x1, y1; // 用于递归返回的中间结果
int gcd = extendedGCD(b, a % b, x1, y1); // 递归调用
x = y1;
y = x1 - (a / b) * y1;
return gcd;
}
int main() {
int a, b;
cout << "输入两个整数 a 和 b: ";
cin >> a >> b;
int x, y;
int gcd = extendedGCD(a, b, x, y);
cout << "gcd(" << a << ", " << b << ") = " << gcd << endl;
cout << "x = " << x << ", y = " << y << endl;
cout << a << " * (" << x << ") + " << b << " * (" << y << ") = " << gcd << endl;
return 0;
}
注意:
递归到最深处, 将人为给出 初始的 x=1, y=0
x = y1;
y = x1 - (a / b) * y1;
这里用的时候死记住, 不好现推