ACM板子总结
ACM
文章目录
二分查找模板
函数实现
#include <iostream>
using namespace std;
// 二分查找函数,查找目标值是否存在
int bsearch(int *a, int n, int x) { // 数组a,长度n,目标值x
int l = 1, r = n; // 左边界l,右边界r
while (l <= r) { // 循环直到左右边界重叠或越界
int m = (l + r) / 2; // 中间位置m
if (a[m] == x) return m; // 找到目标值返回下标
else if (a[m] < x) l = m + 1; // 目标值在右区间
else r = m - 1; // 目标值在左区间
}
return -1; // 未找到目标值返回-1
}
int main() {
int n = 6; // 数组长度
int a[] = {0, 2, 4, 6, 8, 10, 12}; // 数组,a[0]占位
int x = 8; // 要查找的目标值
int pos = bsearch(a, n, x); // 调用二分查找函数
if (pos != -1) cout << "Found at: " << pos << endl;
else cout << "Not found" << endl;
return 0;
}
非函数实现
#include <iostream>
using namespace std;
int main() {
int n = 6; // 数组长度
int a[] = {0, 2, 4, 6, 8, 10, 12}; // 数组,a[0]占位
int x = 8; // 要查找的目标值
int l = 1, r = n; // 左边界l,右边界r
int pos = -1; // 存储目标值下标,默认-1表示未找到
while (l <= r) { // 循环直到左右边界重叠或越界
int m = (l + r) / 2; // 中间位置m
if (a[m] == x) { // 找到目标值
pos = m; // 记录下标
break; // 退出循环
} else if (a[m] < x) l = m + 1; // 目标值在右区间
else r = m - 1; // 目标值在左区间
}
if (pos != -1) cout << "Found at: " << pos << endl;
else cout << "Not found" << endl;
return 0;
}
二分答案法经典题目实现形式
二分答案法的步骤:
确定搜索区间:根据题目要求,设定一个可能的答案范围。
条件判断函数:定义一个函数 check(mid),判断在当前值 mid 时,是否满足条件。
二分查找:使用二分法在答案空间中查找满足条件的值。
如果 check(mid) 返回 true,说明当前 mid 可能是一个可行解,尝试更大的值。
如果 check(mid) 返回 false,说明当前 mid 不是可行解,尝试更小的值。
#include<bits/stdc++.h>
using namespace std;
int n;
long long c;
const int N = 2e5 + 10;
int a[N];
long long check(int m){
long long s = 0;
for(int i = 1;i<=n;i++){
s += (a[i] + 2LL * m) * (a[i] + 2LL * m);
if (s > c) return s;
}
return s;
}
int main() {
cin>>n>>c;
for(int i = 1;i<=n;i++){
cin>>a[i];
}
int l = 1, r = 1e6;
int pos = -1;
while (l <= r) {
int m = (l + r) / 2;
long long s = check(m);
if (s == c) {
pos = m;
break;
} else if (s < c) l = m + 1;
else r = m - 1;
}
cout<<pos;
return 0;
}
整数二分答案法模板(acwing)
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
(满足性质的是右“半”边)
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
(满足性质的是左“半”边)
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
浮点数二分答案法模板
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求(1e-的这个数要比要求的有效位数大2)
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
一维前缀和模板
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int sum[N],a[N];
int n,m;
int l,r;
int main() {
cin>>n>>m; //n为数组a的长度,m为询问次数
for(int i = 1;i<=n;i++){
cin>>a[i];
sum[i] = sum[i-1] + a[i];
}
for(int i = 1;i<=m;i++){
cin>>l>>r; 左区间和右区间
cout<<sum[r] - sum[l-1]<<endl; //区间[l, r]内元素的和,非下标
}
return 0;
}
一维差分处理区间增量问题
#include <iostream>
using namespace std;
int main() {
const int N = 1e5 + 10; //N表示数组的最大长度
int a[N] = {0}, b[N] = {0};
int n, m; //n为数组a的长度,m为询问次数
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
b[i] = a[i] - a[i - 1]; // 构建差分数组b
}
while (m--) {
int l, r, c; // l 和 r 表示操作的区间 [l, r],c 表示加的值
cin >> l >> r >> c;
b[l] += c;
if (r + 1 <= n) { // 如果 r+1 没有越界
b[r + 1] -= c;
}
}
// 根据差分数组 b 计算最终的数组a
for (int i = 1; i <= n; i++) {
a[i] = a[i - 1] + b[i]; // 根据前缀和恢复原数组a
}
for (int i = 1; i <= n; i++) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
二维前缀和模板
#include<bits/stdc++.h>
using namespace std;
const int N = 1e9+100;
int s[N][N],a[N][N];
int x,y,w;
int main() {
int n,m,q;
cin>>n>>m>>q;
for(int i = 1;i<=n;i++){
for(int j =1;j<=m;j++){
cin>>a[i][j];
s[i][j] = s[i-1][j]+ s[i][j-1]+a[i][j] - s[i-1][j-1];//二维前缀和模板数组
}
}
int x1,y1,x2,y2;
for(int i = 1;i<=q;i++){
cin>>x1>>y1>>x2>>y2;
cout<<s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]<<endl; //求从x1,y1到x2,y2的区域和
}
return 0;
}
二维差分增量模板
#include<bits/stdc++.h>
using namespace std;
const int N = 1000 + 10;
int a[N][N], b[N][N];
int n, m, q;
void insert(int x1, int y1, int x2, int y2, int c) {
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
int main() {
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1];
}
}
while (q--) {
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
insert(x1, y1, x2, y2, c);
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
a[i][j] = b[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];
cout << a[i][j] << " ";
}
cout << endl;
}
return 0;
}
其他常用代码片段
遍历所有排列情况
do { } while (next_permutation(v.begin(), v.end()));
清理缓存区
cin.ignore(numeric_limits<streamsize>::max(), '\n');
经典队列操作应用题目
#include<bits/stdc++.h>
using namespace std;
int main() {
int m, n; //m表示缓存最大容量,n表示询问次数
cin >> m >> n; // 读取缓存容量 m 和页面访问次数 n
deque<int> q(m); // 创建一个大小为 m 的双端队列,用来模拟缓存,默认大小为 m
int a; // 用来存储当前访问的页面
int cnt = 0; // 用来记录页面缺失的次数
int qq[1010] = {0}; // 定义一个数组 qq,长度为 1010,初始化所有元素为 0,用来记录哪些页面在缓存中
for (int i = 1; i <= n; i++) { // 遍历所有页面访问
cin >> a; // 读取当前访问的页面号
if (!qq[a]) { // 如果页面 a 不在缓存中
cnt++; // 页面缺失,增加缺页次数
if (q.size() == m) { // 如果缓存已满
int x = q.front(); // 获取队列的第一个元素,即最久未使用的页面
q.pop_front(); // 从队列中移除最久未使用的页面
qq[x] = 0; // 更新数组 qq,标记页面 x 不在缓存中
}
q.push_back(a); // 将当前页面 a 加入队列(缓存)
qq[a] = 1; // 更新数组 qq,标记页面 a 已经在缓存中
}
}
cout << cnt; // 输出缺页次数
return 0; // 程序结束
}
字符串输入
string arr[100];
getline(cin, arr[i]);
自定义排序函数
bool cmp(类型 a, 类型 b) {
// 第一条件: 比较 a 和 b
if (条件1) {
return 排序规则1;
}
// 第二条件: 如果第一条件不满足,比较 a 和 b
if (条件2) {
return 排序规则2;
}
// 第三条件: 如果前面所有条件都不满足,继续比较
if (条件3) {
return 排序规则3;
}
// 默认返回
return 排序规则默认;
}
循环数组索引更新
index = (index + 1) % n; // 因为是循环,使用%运算确保数组循环
解释:
这段代码的含义是在一个固定大小为 n 的数组中循环更新 index,确保它始终保持在合法范围内(0 到 n-1)。它通过取模运算(%)实现循环行为。
具体分析:
1.index + 1:
将当前索引值 index 增加 1,表示向后移动一个位置。
2.% n:
模运算确保索引不会超出数组的边界。如果增加后索引等于或大于 n,模运算会使它“回到”开头。
例如:
2.1 如果 index + 1 = n,则 (index + 1) % n = 0。
2.2 如果 index + 1 = n + 1,则 (index + 1) % n = 1。
3.循环效果:
模运算的结果总是一个小于 n 的非负数,这样可以实现数组的循环访问。
3.1 当 index 为数组最后一个位置(n-1)时,执行 (index + 1) % n 会将索引跳转到第一个位置 0。
3.2 否则,索引会正常向后移动。
注意做题时样例输入陷阱,特殊样例,比如数组长度为0,尤其注意题目给的范围,比如>=
格式化输出
cout << setfill('0') << setw(2) << sum;
cout << fixed << setprecision(2) << num << endl;
输出32位二进制形式
cout << bitset<32>(n) << endl;
cin >> oct >> n; // 从输入以八进制形式读取一个整数
cout << dec << n; // 以十进制形式输出该整数
sawp函数不要忘了使用
注意样例空格
字符串和整数转换
以字符串的形式输入数组进行数字的运算时,字符’0’实际上是48
注意在C++中,字符和整数之间可以进行转换。字符’0’到’9’的ASCII码分别是48到57。当你从一个字符中减去’0’时,实际上是将该字符转换为对应的数字。
注意int long范围,数组可能是double类型等等
字符数组长度
char a[100];
int b = strlen(a);
字符串长度
string a;
int b = a.size();
宏定义
#define 宏名 替换内容
定义尽量都在主函数外定义
类型别名
using ll = long long;
typedef long long ll;
结构体数组示例(贪心排序题可能用到)
#include <iostream>
#include <algorithm> // 包含 sort 函数
using namespace std;
struct Point {
int x, y;
};
// 自定义比较函数
bool cmp(const Point &a, const Point &b) {
if (a.x == b.x) {
return a.y < b.y; // 如果 x 相同,按 y 排序
}
return a.x < b.x; // 否则按 x 排序
}
int main() {
Point points[3];
// 通过 cin 输入结构体数组
for (int i = 0; i < 3; i++) {
cin >> points[i].x >> points[i].y;
}
// 使用自定义比较函数进行排序
sort(points, points + 3, cmp);
// 输出排序后的数组
for (int i = 0; i < 3; ++i) {
cout << "points[" << i << "]: x=" << points[i].x << ", y=" << points[i].y << endl;
}
return 0;
}
lower_bound
和 upper_bound
lower_bound 是 C++ 标准库 中的一个非常有用的函数,它用于在已排序的容器中查找第一个不小于(即大于或等于)给定值的元素的位置。它可以用于数组、vector、deque、set 和 map 等支持随机访问或二叉搜索的数据结构。
(容器中的元素必须是已排序的。lower_bound 使用二分查找,因此只有在排序容器中才能正确工作。)
auto 是 C++11 引入的一个关键字,用于自动推导变量的类型。通过使用 auto,编译器可以根据变量初始化时的值自动推导出该变量的类型。这使得代码更加简洁,特别是在处理复杂的类型时(例如迭代器或类型较长的容器元素)。
#include <iostream>
#include <algorithm>
using namespace std;
vector<int> vec = {1, 3, 3, 5, 7, 9};
// 查找第一个不小于 3 的位置(即第一个 3)
auto lb = lower_bound(vec.begin(), vec.end(), 3);
// 查找第一个大于 3 的位置(即第一个大于 3 的元素位置)
auto ub = upper_bound(vec.begin(), vec.end(), 3);
//lb 的位置是 vec.begin() + 1,即指向第一个 3。
//ub 的位置是 vec.begin() + 3,即指向第一个大于 3 的元素 5。
cout << "lb: " << (lb - vec.begin()) << endl; // 输出 1,表示第一个 3
cout << "ub: " << (ub - vec.begin()) << endl; // 输出 3,表示 5
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
long long a[N], b[N];
int main() {
int n, m, sum;
cin >> n >> m >> sum;
// 输入数组 a[] 和 b[]
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 0; i < m; i++) {
cin >> b[i];
}
// 对数组 b[] 进行排序
sort(b, b + m);
// 遍历数组 a[],对于每个 a[i] 使用二分查找
for (int i = 0; i < n; i++) {
long long target = sum - a[i];
// 在 b[] 中查找 target
int idx = lower_bound(b, b + m, target) - b;
// 检查是否找到并且满足 a[i] + b[idx] == sum
if (idx < m && b[idx] == target) {
cout << i << " " << idx << endl;
return 0; // 找到后直接退出
}
}
// 如果没有找到符合条件的 pair
cout << -1 << endl;
return 0;
}
STL 常用函数
最值
max(x, y); // 返回 x 和 y 较大值
min(x, y); // 返回 x 和 y 较小值
排序
sort(va.begin(), va.end(), cmp);
子串操作
#include <iostream>
#include <cstring> // 包含 C 字符串处理函数 strstr
using namespace std;
int main() {
//子串截取
string s = "Hello, World!";
string sub = s.substr(7, 5); // 从下标 7 开始截取 5 个字符
s.erase(7, 10); // 从下标7 删除 10 个字符
//查找子串
size_t pos = s.find("World"); // 查找 "World" 的位置,一般返回第一个字母起始下标
if (pos != string::npos) {
cout << "Found at: " << pos << endl; // 如果找到,输出位置
}
// 定义两个字符数组,用于存储输入的源字符串和需要查找的子串
char str[100]; // 源字符串
char target[100]; // 子串
cin.getline(str, 100); // 使用 getline 读取一整行字符串
cin.getline(target, 100); // 输入需要查找的子串
const char* pos = strstr(str, target); // 使用 strstr 函数查找子串在源字符串中的位置
// 判断是否找到子串
if (pos != nullptr) {
// 如果找到,计算子串的起始下标,并输出
cout << "Substring found at index: " << (pos - str) << endl;
} else {
cout << "Substring not found!" << endl;
}
return 0;
}
双指针模板
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n, x; // n为数组大小,x为目标和
cin >> n >> x; // 输入n和目标和x
int a[n + 1]; // 数组,从1开始存储
for (int i = 1; i <= n; i++) {
cin >> a[i]; // 输入数组
}
sort(a + 1, a + n + 1); // 排序数组
int i = 1, j = n; // 双指针初始化
while (i < j) {
int s = a[i] + a[j]; // 当前两个数的和
if (s == x) {
cout << a[i] << " " << a[j] << endl;
break;
} else if (s < x) {
i++; // 左指针右移
} else {
j--; // 右指针左移
}
}
if (i >= j) {
cout << "No solution" << endl; // 没有找到满足条件的数对
}
return 0;
}
1.7.0输出单词
给定一行句子,输出其中的单词
样例输入
i am student
样例输出
i
am
student
#include <bits/stdc++.h>
#include <string>
using namespace std;
int main()
{
char str[1000];
// 使用 gets() 读取一行字符串(注意:gets() 已被弃用,推荐使用 fgets() 或 getline())
gets(str);
// 获取字符串的长度
int n = strlen(str);
// 遍历字符串,逐个输出单词
for (int i = 0; i < n; i++)
{
int j = i;
// 指针 j 扫描字符串,直到遇到空格或者字符串结束
while (j < n && str[j] != ' ')
j++; // j 指向当前单词结束的位置(空格处或者字符串末尾)
// 输出当前单词(从 i 到 j-1)
for (int k = i; k < j; k++)
cout << str[k];
cout << endl; // 输出换行符,每个单词单独占一行
// 将 i 移动到 j 处,即空格后的第一个字符位置
// 注意:这里 i 在 for 循环的自增中会自动 +1,因此如果想跳过空格,
// 可能需要额外处理多个连续空格的情况(当前代码假设单词间仅有一个空格)
i = j;
}
return 0;
}
注意事项补充
不要忘了把oj的编译模式换成C++
一定注意输入的是n的范围还是n个整数的范围!!!!
注意输入样例形式,如1111与1 1 1 1你就要考虑不同的接收输入的方式了
别把2*i写成2i
为防止爆long long ,需对等式右边进行强转
如:s += (a[i] + 2LL * m) * (a[i] + 2LL * m);
定义cmp时,不要带等号
使用auto遍历容器时,要改变容器内元素加&,不改变不加.
continue用法示例
//数字反转,消除原数末尾0,但中间0不动.
int flag = 1;
for(int i = p-1;i>=0;i--){
if(a[i]=='0' && flag==1 && i>0){
continue;
}
flag = 0;
cout<<a[i];
}
字符串经典操作例题(包含回文,去重,取子串)
#include<bits/stdc++.h>
using namespace std;
string s;
string zican(string sub){
string a = "";
a+=sub[0];
for(int i = 0;i<sub.size()-1;i++){
if(sub[i+1]!=sub[i]){
a += sub[i+1];
}
}
return a;
}
bool huiwen(string subb){
int i = 0;
int j = subb.size()-1;
while(i<=j){
if(subb[i]!=subb[j]){
return false;
}
i++;
j--;
}
return true;
}
int main() {
long long sumj = 0;
long long sumo = 0;
cin>>s;
int n = s.size();
for(int i = 0;i<n;i++){
for(int j = 1;i+j<=n;j++){
string sub = s.substr(i,j);
string subb = zican(sub);
if(huiwen(subb)){
if(j%2==0){
sumo++;
}else{
sumj++;
}
}
}
}
cout<<sumo<<" "<<sumj;
return 0;
}
C风格字符串处理
- int result = strcmp(str1, str2);
strcmp 用于比较两个 C 风格字符串(即 char 数组)
返回 0:如果两个字符串相等。
返回一个负整数:如果 str1 小于 str2(按字典顺序比较)。
返回一个正整数:如果 str1 大于 str2(按字典顺序比较). - strcpy 用于将一个 C 风格字符串的内容复制到另一个字符串中.
char* strcpy(char* dest, const char* src);
dest:目标字符数组,拷贝的结果会存储在这里。
src:源字符数组,即你要复制的字符串。
返回值:返回目标字符串 dest 的指针。 - strcat 用于将一个字符串连接到另一个字符串的末尾。
char* strcat(char* dest, const char* src);
dest:目标字符数组,连接结果将存储在此。
src:源字符数组,要追加的字符串。
返回值:返回目标字符串 dest 的指针。 - strchr 用于查找字符串中第一次出现指定字符的位置。
str:要查找的字符串。
ch:要查找的字符。
返回值:返回指向找到的字符的指针,如果没有找到,则返回 nullptr。
#include <iostream>
#include <cstring>
using namespace std;
int main() {
const char* str = "Hello, world!";
char* pos = strchr(str, 'o'); // 查找字符 'o'
if (pos != nullptr) {
cout << "Found 'o' at position: " << (pos - str) << endl; // 计算相对位置
} else {
cout << "'o' not found!" << endl;
}
return 0;
}
输出:Found ‘o’ at position: 4
- strtok 用于分割字符串,它根据指定的分隔符把一个字符串分解为多个子字符串。
char* strtok(char* str, const char* delimiters);
str:待分割的字符串。首次调用时需要传入原始字符串,后续调用可以传入 nullptr 来继续分割。
delimiters:用于分割的分隔符(多个字符)。
返回值:返回指向子字符串的指针。
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char str[] = "Hello, world, C++!";
char* token = strtok(str, ", "); // 以 ", " 作为分隔符
while (token != nullptr) {
cout << "Token: " << token << endl;
token = strtok(nullptr, ", "); // 继续分割
}
return 0;
}
输出:Token: Hello
Token: world
Token: C++
- strncat 类似于 strcat,但它会限制连接的字符数量,防止溢出
dest:目标字符数组。
src:源字符串。
n:要连接的最大字符数。
快速幂算法
#include <iostream>
using namespace std;
// 快速幂函数:计算 a^b
int qp(int a, int b) {
int r = 1; // r 存储结果,初始为 1
while (b > 0) { // 当指数 b 大于 0 时继续循环
if (b % 2) // 如果 b 是奇数
r *= a; // 将当前基数累乘到结果
a *= a; // 基数自乘
b /= 2; // 指数减半
}
return r; // 返回计算结果
}
int main() {
int a, b;
cout << "输入底数和指数:";
cin >> a >> b; // 输入底数 a 和指数 b
cout << a << "^" << b << " = " << qp(a, b) << endl;
return 0;
}
------------------------------------------------------------------------------
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
// 定义长整型别名,便于后续使用
typedef long long LL;
// 快速幂函数,计算 (a^k) % p
int qmi(int a, int k, int p)
{
int res = 1; // 初始化结果为 1
while (k) // 当指数 k 不为 0 时循环
{
if (k & 1) // 如果 k 是奇数,累乘当前的 a 并取模
res = (LL)res * a % p;
k /= 2; // 指数 k 右移一位,相当于整除 2
a = (LL)a * a % p; // 底数自乘并取模
}
return res; // 返回计算结果
}
int main()
{
int n;
cin >> n; // 读取测试用例数量
while (n--) // 循环处理每个测试用例
{
int a, k, p;
cin >> a >> k >> p; // 读取底数 a,指数 k 和模数 p
cout << qmi(a, k, p) << endl; // 输出快速幂结果
}
return 0;
}
快速幂
4.4.1快速幂
时间复杂度为O(log n)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long LL;
int qmi(int a,int k,int p)
{
int res=1;
while(k)
{
if(k&1) res=(LL)res*a%p;
k/=2;
a=(LL)a*a%p;
}
return res;
}
int main()
{
int n;
cin>>n;
while(n--)
{
int a,k,p;
cin>>a>>k>>p;
cout<<qmi(a,k,p)<<endl;
}
return 0;
}
accumulate
vector<int> vec = {1, 2, 3, 4, 5};
// 计算 vec 中所有元素的和,初始值为 0
int sum = accumulate(vec.begin(), vec.end(), 0);
- 还通过提供自定义的二元操作 op,可以实现不同的聚合操作。例如,计算区间内所有元素的乘积。
vector<int> vec = {1, 2, 3, 4, 5};
// 使用乘法作为操作符计算元素的乘积,初始值为 1
int product = accumulate(vec.begin(), vec.end(), 1, multiplies<int>());
//multiplies<int>() 是 C++ 标准库中提供的一个函数对象,它执行乘法操作。
- accumulate 也可以用于其他类型的数据聚合,例如字符串的拼接。通过传入一个适当的操作函数(如加法运算符),可以将一个字符串序列拼接成一个完整的字符串。
vector<string> words = {"Hello", " ", "World", "!"};
// 使用字符串拼接操作,将所有字符串拼接起来
string result = accumulate(words.begin(), words.end(), string());
- 如果我们想要找出一组元素中的最大值,也可以使用 accumulate,配合自定义的操作函数(如 std::max)。
vector<int> vec = {1, 9, 3, 7, 5};
// 使用 max 来查找最大值
int max_value = accumulate(vec.begin(), vec.end(), vec[0], max<int>());
单调栈模板
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
-------------------------------------------------------------------------------------------------------------
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
scanf("%d", &x);
while (tt && stk[tt] >= x) tt -- ;//如果栈顶元素大于当前待入栈元素,则出栈
if (!tt) printf("-1 ");//如果栈空,则没有比该元素小的值。
else printf("%d ", stk[tt]);//栈顶元素就是左侧第一个比它小的元素。
stk[ ++ tt] = x;
}
return 0;
}
单调栈stl实现
#include <iostream>
#include <stack>
using namespace std;
int main() {
int n;
cin >> n;
stack<int> st; // 使用 STL 的栈容器
while (n--) {
int x;
cin >> x; // 读入当前数字
// 将栈中所有大于等于 x 的元素弹出
while (!st.empty() && st.top() >= x)
st.pop();
// 如果栈为空,则说明左侧没有比 x 小的数
if (st.empty())
cout << "-1 ";
else
cout << st.top() << " "; // 栈顶元素就是左侧第一个比 x 小的数
// 将当前数字入栈,作为后续数字的候选者
st.push(x);
}
return 0;
}
深度优先搜索(DFS)
dfs基本应用:类似树的形式,一直向深处搜索,然后进行回溯。
#include<bits/stdc++.h>
using namespace std;
int n; // 用于存储输入的整数 n,代表全排列的元素个数
bool st[1000]; // 用于标记某个数字是否已经被使用
int path[1000]; // 用于存储当前的排列路径
// 深度优先搜索函数,用于生成全排列
void dfs(int u)
{
if (u == n) // 如果当前排列的长度已经达到 n
{
for (int i = 0; i < n; i++) // 输出当前排列
printf("%d ", path[i]);
puts(""); // 换行
return;
}
// 枚举从 1 到 n 的所有数字
for (int i = 1; i <= n; i++)
if (!st[i]) // 如果数字 i 尚未被使用
{
path[u] = i; // 将数字 i 放到当前排列的位置 u
st[i] = true; // 标记数字 i 已经被使用
dfs(u + 1); // 递归调用,进入下一层
st[i] = false; // 回溯,撤销数字 i 的使用标记
}
}
int main()
{
cin >> n; // 输入整数 n
dfs(0); // 从第 0 层开始进行深度优先搜索
return 0;
}
---------------------------------------------------------------------------
#include <bits/stdc++.h>
using namespace std;
int n; // 输入的整数 n,代表全排列的元素个数
int main() {
cin >> n; // 输入整数 n
stack<pair<vector<int>, vector<bool>>> stk;
// 栈中存储的元素为:当前排列路径和数字使用标记
// 初始化栈,开始时路径为空,所有数字均未被使用
vector<int> path; // 当前排列路径
vector<bool> used(n + 1, false); // 标记数组,n+1大小便于直接用数字 1 到 n
stk.push({path, used}); // 压入初始状态
while (!stk.empty()) { // 栈非空时循环
auto [path, used] = stk.top(); // 取出栈顶状态
stk.pop(); // 弹出栈顶
if (path.size() == n) { // 如果当前排列长度达到 n,则输出排列
for (int x : path) cout << x << " ";
cout << endl;
continue;
}
// 枚举从 1 到 n 的所有数字,尝试扩展路径
for (int i = n; i >= 1; --i) { // 倒序枚举,保证路径扩展时顺序正确
if (!used[i]) { // 如果数字 i 尚未被使用
vector<int> new_path = path; // 当前路径的副本
vector<bool> new_used = used; // 当前标记数组的副本
new_path.push_back(i); // 添加数字 i 到路径
new_used[i] = true; // 标记数字 i 已被使用
stk.push({new_path, new_used}); // 将新状态压入栈
}
}
}
return 0;
}
n皇后问题中,一个坐标是u,i的点,在u+i这条对角线上,也在n-u+i这条反对角线上。
广度优先搜索(BFS)
bfs:一层一层地向外进行扩展,直到搜到终点位置,本质是队列,最先搜到的位置一定是最短路径,所以bfs有最短路径。
#include <bits/stdc++.h>
using namespace std;
const int N = 110; // 假设网格的最大边长为 110
typedef pair<int, int> PII; // 定义坐标点的类型 (x, y)
// 定义全局变量
int d[N][N]; // 距离数组,存储每个点到起点的最短路径长度
int g[N][N]; // 网格数组,输入地图
bool st[N][N]; // 标记数组,判断某点是否已经访问过
int n, m; // 网格的大小 n 行 m 列
PII Pre[N][N]; // 记录路径的前驱节点,用于输出路径
// 宽度优先搜索(BFS)
int bfs() {
queue<PII> q; // 定义队列
q.push({1, 1}); // 把起始位置放入队列
st[1][1] = true; // 标记起点已访问
d[1][1] = 0; // 起点到起点的距离为 0
// 定义方向数组,用于上下左右的四个方向
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
while (!q.empty()) { // 当队列不为空
auto t = q.front(); // 取出队头元素
q.pop(); // 弹出队头元素
for (int i = 0; i < 4; i++) { // 枚举四个方向
int x = t.first + dx[i], y = t.second + dy[i]; // 新的坐标
// 检查新坐标是否有效:1. 不越界;2. 是空地;3. 没访问过
if (x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] == 0 && !st[x][y]) {
d[x][y] = d[t.first][t.second] + 1; // 更新距离
Pre[x][y] = t; // 记录路径的前驱节点
q.push({x, y}); // 新点入队
st[x][y] = true; // 标记新点已访问
}
}
}
/*
如果需要输出路径,可以启用这段代码:
int x = n, y = m; // 从终点回溯路径
while (x || y) {
cout << x << " " << y << endl; // 输出路径点
auto t = Pre[x][y]; // 找到当前点的前驱
x = t.first, y = t.second; // 回到前驱点
}
*/
return d[n][m]; // 返回终点的最短路径长度
}
int main() {
// 输入网格大小
scanf("%d%d", &n, &m);
// 输入网格数据
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> g[i][j];
// 输出从起点到终点的最短路径长度
printf("%d\n", bfs());
return 0;
}
cin.tie(0);和ios::sync_with_stdio(false);可以提升效率。
单链表
实现一个单链表,链表初始为空,支持三种操作:
向链表头插入一个数;
删除第 k 个插入的数后面的数;
在第 k个插入的数后插入一个数。
现在要对该链表进行 M次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x,表示向链表头插入一个数 x。
D k,表示删除第 k个插入的数后面的数(当 k为 0 时,表示删除头结点)。
I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
1≤M≤100000
所有操作保证合法。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
我们用-1表示空指针。
实现一些基本的操作:
(1)初始化
头节点指向-1表示空节点,idx=0表示从0好节点进行编号。
void init()//链表的初始化
{
head=-1;//头节点指向空节点
idx=0;
}
(2)向头节点后面插入一个新节点
(3)向第k个插入的点后面添加一个点同(2)
void add(int k,int x)//向第k个插入的数后面插入一个数
{
e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}
因为是从0号节点进行编号的,所以第k个插入的点其实是第k-1个点add(k-1,x);
(4)删除头节点
void remove()//删除头节点
{
head=ne[head];
}
(5)删除第k个插入的点
void de(int k)//删除第k个插入的数
{
ne[k]=ne[ne[k]];
}
remove(k-1);
AC代码
#include<bits/stdc++.h>
#include<string>
using namespace std;
const int N=1e6+10;
int head,e[N],ne[N],idx;
void init()//链表的初始化
{
head=-1;
idx=0;
}
void add_head(int x)//向头节点之后插入一个数
{
e[idx]=x,ne[idx]=head,head=idx++;
}
void add(int k,int x)//向第k个插入的数后面插入一个数
{
e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}
void de(int k)//删除第k个插入的数
{
ne[k]=ne[ne[k]];
}
void remove()//删除头节点
{
head=ne[head];
}
int main()
{
int t;
scanf("%d",&t);
init();
while(t--){
string op;
int k,x;
cin>>op;
if(op=="H"){
scanf("%d",&x);
add_head(x);
}
else if(op=="D"){
scanf("%d",&k);
if(k==0) remove();
de(k-1);
}
else{
scanf("%d%d",&k,&x);
add(k-1,x);
}
}
for(int i=head;i!=-1;i=ne[i])
cout<<e[i]<<" ";
return 0;
}
快速排序
#include<bits/stdc++.h>
using namespace std;
const int N=100010; // 定义数组最大容量
int q[N]; // 静态分配数组,避免动态内存开销
/**
* 快速排序函数(双指针法)
* @param q[] 待排序数组
* @param l 当前区间的左边界
* @param r 当前区间的右边界
* 时间复杂度: 平均O(nlogn),最坏O(n²)
* 空间复杂度: O(logn) 递归栈空间
*/
void quick_sort(int q[], int l, int r) {
// 递归终止条件:区间长度<=1时无需排序
if (l >= r) return;
// 选取中间元素作为基准值(比经典选首元素更抗退化)
int x = q[l + r >> 1]; // 位运算等效 (l+r)/2
int i = l - 1; // 左扫描指针(从界外开始)
int j = r + 1; // 右扫描指针(从界外开始)
// 核心分区逻辑:将数组分为<=x和>=x的两部分
while (i < j) {
// 找到左边第一个 >=x 的元素
do i++; while (q[i] < x); // 注意没有等号,保证稳定性
// 找到右边第一个 <=x 的元素
do j--; while (q[j] > x); // 注意没有等号,保证稳定性
// 当指针未交叉时交换元素
if (i < j) swap(q[i], q[j]);
}
// 递归处理子区间(选择j作为分界点保证区间分裂)
quick_sort(q, l, j); // 处理左半区间
quick_sort(q, j + 1, r); // 处理右半区间
}
int main() {
int n;
cin >> n;
// 输入数据
for (int i = 0; i < n; i++)
cin >> q[i];
// 调用快速排序
quick_sort(q, 0, n - 1);
// 输出排序结果
for (int i = 0; i < n; i++)
cout << q[i] << " ";
return 0;
}
归并排序
#include <bits/stdc++.h>
using namespace std;
const int N = 100010; // 定义数组的最大容量
int q[N], tmp[N]; // q 数组用于存放待排序的数据,tmp 数组用于归并排序时的临时存储
// 归并排序函数,参数 q 是数组,l 和 r 分别是当前排序区间的左右边界索引
void merge_sort(int q[], int l, int r) {
// 如果区间内只有一个元素或无元素,则无需排序,直接返回
if(l >= r) return;
// 计算中间位置
int mid = l + r >> 1; // 注意:这里等同于 mid = (l + r) / 2
// 对左右两个子区间分别进行归并排序
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);
// 合并两个已排序的子区间
int k = 0; // tmp 数组的索引
int i = l, j = mid + 1; // 两个子区间的起始索引
// 当两个子区间都未遍历完时,比较两边元素,将较小的元素存入 tmp 数组中
while(i <= mid && j <= r)
if(q[i] < q[j])
tmp[k++] = q[i++]; // 如果左边元素较小,存入 tmp,并移动左边索引
else
tmp[k++] = q[j++]; // 否则存入右边元素,并移动右边索引
// 将剩余的左子区间元素存入 tmp(如果有剩余的话)
while(i <= mid)
tmp[k++] = q[i++];
// 将剩余的右子区间元素存入 tmp(如果有剩余的话)
while(j <= r)
tmp[k++] = q[j++];
// 将排好序的 tmp 数组复制回原数组对应位置
for(i = l, j = 0; i <= r; i++, j++)
q[i] = tmp[j];
}
int main()
{
int n;
// 读取数据个数
scanf("%d", &n);
// 读取 n 个整数存入数组 q
for(int i = 0; i < n; i++)
scanf("%d", &q[i]);
// 对整个数组进行归并排序
merge_sort(q, 0, n - 1);
// 输出排序后的数组
for(int i = 0; i < n; i++)
cout << q[i] << " ";
return 0;
}
位运算
&:按位与,1&0=0,0&1=0,0&0=0,1&1=1,只有都为1时才为1.
|:按位或,1|1=1,1|0=1,0|1=1,0|0=0,只有都为0时才为0.
^:按位异或,1^1=0,1^0=1,0^a=a,相同为0,不同为非0的那个数.
>>:右移,a>>x,表示a除以2^x;
<<:左移,a<<x,表示a乘2^x;
~:把0变成1,把1变成0;
-x=~x+1;
(1)lowbit(x)
将十进制数的二进制表示的最低位1取出来。
int lowbit(int x)
{
return x&-x;
}
如x的二进制表示时100,-x在计算机中为~x+1,则~x=011,~x+1=111,那么就有
(100)&(111)=(100),这样就可以把最低位上面的1取出来。
(2)把n对应二进制表示中第k位取出来(注意有第0位)
int get(int n,int k)
{
return n>>k&1;
}
(3)输出所有小于k的十进制
for(int i=0;i<1<<k;i++)
cout<<i;
区间合并
给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。
输入格式
第一行包含整数 nn。
接下来 nn 行,每行包含两个整数 l 和 r。
输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。
数据范围
1≤n≤100000,
−109≤li≤ri≤109
输入样例:
5
1 2
2 4
5 6
7 8
7 9
输出样例:
3
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 定义一个 pair 类型,表示区间,first 表示区间的左端点,second 表示右端点
typedef pair<int, int> PII;
int n; // 区间的数量
// merge 函数用于合并重叠的区间,输入参数 interval 为区间集合
void merge(vector<PII> &interval)
{
// 用 ans 存储合并后的区间结果
vector<PII> ans;
// 对区间进行排序,排序规则是先按照区间的左端点升序,
// 如果左端点相同,则按照右端点升序排序
sort(interval.begin(), interval.end()); //! pair排序 优先左端点, 再以右端点排序
// 初始化当前区间的左右边界 st 和 ed
// 初始化为一个很小的值,确保第一次比较时一定满足条件 ed < item.first
int st = -1e9 - 10, ed = -1e9 - 10; //! 只要比 -1e9 小就可以
// 遍历排序后的区间
for(auto item : interval)
{
// 如果当前区间与遍历到的区间没有重叠(即当前区间的结束点小于新区间的起始点)
if(ed < item.first)
{
// 如果 st 不是初始值,则说明前面存在一个合法区间,加入 ans
if(st != -1e9 - 10)
ans.push_back({st, ed}); //! 第一次在这里初始化
// 更新当前区间为新区间
st = item.first;
ed = item.second; //! 第一段区间从这里开始
}
else
{
// 如果有重叠,则更新当前区间的结束点为两个区间结束点的最大值
ed = max(ed, item.second);
}
}
// todo 这个循环结束之后还会剩下一个未加入的区间
// 最后一次合并后的区间需要加入结果中
if(st != -1e9 - 10)
ans.push_back({st, ed}); //! 如果不是空的 那我们就加上一段
// 更新输入的区间集合为合并后的结果
interval = ans;
}
int main(void)
{
// 提高输入输出效率
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 输入区间数量
cin >> n;
// 定义一个 vector 用来存储所有区间
vector<PII> interval;
while(n--)
{
int l, r;
// 输入每个区间的左右端点
cin >> l >> r;
// 将输入的区间加入集合
interval.push_back({l, r});
}
// 调用 merge 函数合并所有重叠区间
merge(interval);
// 输出合并后区间的数量
cout << interval.size() << endl;
return 0;
}
用数组模拟链表。
单链表
实现一个单链表,链表初始为空,支持三种操作:
向链表头插入一个数;
删除第 k 个插入的数后面的数;
在第 k个插入的数后插入一个数。
现在要对该链表进行 M次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x,表示向链表头插入一个数 x。
D k,表示删除第 k个插入的数后面的数(当 k为 0 时,表示删除头结点)。
I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
1≤M≤100000
所有操作保证合法。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
我们用-1表示空指针。
实现一些基本的操作:
(1)初始化
头节点指向-1表示空节点,idx=0表示从0好节点进行编号。
void init()//链表的初始化
{
head=-1;//头节点指向空节点
idx=0;
}
(2)向头节点后面插入一个新节点
(3)向第k个插入的点后面添加一个点同(2)
void add(int k,int x)//向第k个插入的数后面插入一个数
{
e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}
因为是从0号节点进行编号的,所以第k个插入的点其实是第k-1个点add(k-1,x);
(4)删除头节点
void remove()//删除头节点
{
head=ne[head];
}
(5)删除第k个插入的点
void de(int k)//删除第k个插入的数
{
ne[k]=ne[ne[k]];
}
remove(k-1);
AC代码
#include<bits/stdc++.h>
#include<string>
using namespace std;
const int N=1e6+10;
int head,e[N],ne[N],idx;
void init()//链表的初始化
{
head=-1;
idx=0;
}
void add_head(int x)//向头节点之后插入一个数
{
e[idx]=x,ne[idx]=head,head=idx++;
}
void add(int k,int x)//向第k个插入的数后面插入一个数
{
e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}
void de(int k)//删除第k个插入的数
{
ne[k]=ne[ne[k]];
}
void remove()//删除头节点
{
head=ne[head];
}
int main()
{
int t;
scanf("%d",&t);
init();
while(t--){
string op;
int k,x;
cin>>op;
if(op=="H"){
scanf("%d",&x);
add_head(x);
}
else if(op=="D"){
scanf("%d",&k);
if(k==0) remove();
de(k-1);
}
else{
scanf("%d%d",&k,&x);
add(k-1,x);
}
}
for(int i=head;i!=-1;i=ne[i])
cout<<e[i]<<" ";
return 0;
}
双链表
实现一个双链表,双链表初始为空,支持 55 种操作:
在最左侧插入一个数;
在最右侧插入一个数;
将第 k 个插入的数删除;
在第 k 个插入的数左侧插入一个数;
在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
L x,表示在链表的最左端插入数 x。
R x,表示在链表的最右端插入数 x。
D k,表示将第 k 个插入的数删除。
IL k x,表示在第 k 个插入的数左侧插入一个数。
IR k x,表示在第 k 个插入的数右侧插入一个数。
输出格式
共一行,将整个链表从左到右输出。
数据范围
1≤M≤100000
所有操作保证合法。
输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9
双链表类似单链表的操作进行处理,只是每个节点都有两个指针l[],r[],分别指向前驱和后继。
模板:
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
#include<bits/stdc++.h>
#include<string>
#include<algorithm>
using namespace std;
const int N=1e6+10;
int l[N],r[N],e[N],idx;
void init()
{
r[0]=1;
l[1]=0;
idx=2;
}
void add(int k,int x)
{
e[idx]=x;
r[idx]=r[k];
l[idx]=k;
l[r[k]]=idx;
r[k]=idx;
idx++;
}
void remove(int k)
{
r[l[k]]=r[k];
l[r[k]]=l[k];
}
int main()
{
init();
int t;
cin>>t;
while(t--)
{
string op;
cin>>op;
int k,x;
if(op=="R")
{
cin>>x;
add(l[1],x);
}
else if(op=="L")
{
cin>>x;
add(0,x);
}
else if(op=="D")
{
cin>>k;
remove(k+1);
}
else if(op=="IL")
{
cin>>k>>x;
add(l[k+1],x);
}
else
{
cin>>k>>x;
add(k+1,x);
}
}
for(int i=r[0];i!=1;i=r[i])
cout<<e[i]<<" ";
return 0;
}
栈
模拟栈
实现一个栈,栈初始为空,支持四种操作:
push x – 向栈顶插入一个数 x;
pop – 从栈顶弹出一个数;
empty – 判断栈是否为空;
query – 查询栈顶元素。
现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式
对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示栈顶元素的值。
数据范围
1≤M≤100000,
1≤x≤1e9
所有操作保证合法。
输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO
栈:后进先出的数据结构。
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[ ++ tt] = x;
// 从栈顶弹出一个数
tt -- ;
// 栈顶的值
stk[tt];
// 判断栈是否为空
if (tt > 0)
{
}
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int stk[N],tt=0;
int main()
{
int t;
cin>>t;
while(t--)
{
string op;
cin>>op;
if(op=="push")
{
int x;
cin>>x;
stk[++tt]=x;
}
else if(op=="pop")
{
tt--;
}
else if(op=="empty")
{
if(tt>0)
cout<<"NO"<<endl;
else
cout<<"YES"<<endl;
}
else if(op=="query")
{
cout<<stk[tt]<<endl;
}
}
return 0;
}
队列
实现一个队列,队列初始为空,支持四种操作:
push x – 向队尾插入一个数 x;
pop – 从队头弹出一个数;
empty – 判断队列是否为空;
query – 查询队头元素。
现在要对队列进行 M个操作,其中的每个操作 3和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式
对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示队头元素的值。
数据范围
1≤M≤100000,
1≤x≤1e9,
所有操作保证合法。
输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
队列:先进先出。
普通队列
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
// 向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++ ;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh <= tt)
{
}
循环队列
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;
// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;
// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh != tt)
{
}
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int q[N],hh=0,tt=-1;
int main()
{
ios::sync_with_stdio(false);
int T;
cin>>T;
while(T--){
string op;
cin>>op;
if(op=="push"){
int x;
cin>>x;
q[++tt]=x;
}
else if(op=="pop"){
hh++;
}
else if(op=="empty"){
if(hh<=tt) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
else cout<<q[hh]<<endl;
}
return 0;
}
单调队列
滑动窗口
给定一个大小为 n≤1e6的数组。
有一个大小为 k的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 kk 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
性质:队列里面的元素值是单调的,递增或者递减。
思想:
例如:求滑动窗口的最大值。
用单调队列储存当前窗口内单调递减的元素的下标,并且队头是窗口内的最大值,队尾是窗口内的尾元素。也就是说,队列从队头到队尾对应窗口内从最大值到窗口的尾元素的子序列下标。
1.队头出队:当队头元素从滑动窗口划出时,队头元素出队,hh++。
2.队尾出队:当新的元素进入滑动窗口时,要把新元素从队尾插入,分两种情况:
(1).直接插入:如果新元素小于队尾元素,那么直接从队尾插入(q[++tt]=i),因为他可能在前面的最大值滑出窗口后成为最大值。
(2).先删后插:如果新元素大于等于队尾元素,那就先删除队尾元素(因为队尾不可能成为滑动窗口的最大值),删除队尾tt–,循环删除,直到队列为空或遇到一个大于新元素的值,再插入。
求最小值的思路相同。
AC代码
#include<iostream>
using namespace std;
const int N = 1e6+10;
int a[N],q[N];
int n,k;
int main()
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i];
//求滑动窗口里面的最小值。
int hh=0,tt=-1;
for(int i=1;i<=n;i++)
{
if(hh<=tt&&q[hh]<i-k+1) hh++; //如果队头元素值表示序列的下表不在滑动窗口的范围内,队头出队。
while(hh<=tt&&a[i]<=a[q[tt]]) tt--; //如果插入的元素小于队尾元素,队尾出队,直到不小于为止。
q[++tt]=i; //下表入队
if(i>k-1) cout<<a[q[hh]]<<" "; //如果在滑动窗口的范围,输出最小值即可。
}
puts("");
//求滑动窗口里面的最大值
hh=0,tt=-1;
for(int i=1;i<=n;i++)
{
if(hh<=tt&&q[hh]<i-k+1) hh++;
while(hh<=tt&&a[i]>=a[q[tt]]) tt--;
q[++tt]=i;
if(i>k-1) cout<<a[q[hh]]<<" ";
}
return 0;
}
单调队列stl实现
#include <iostream>
#include <deque>
#include <vector>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// 求滑动窗口最小值
deque<int> dqMin; // 存放下标,保证对应的 a 值单调递增
for (int i = 0; i < n; i++) {
// 移除不在窗口中的下标
if (!dqMin.empty() && dqMin.front() <= i - k)
dqMin.pop_front();
// 移除所有比当前数 a[i] 大的下标
while (!dqMin.empty() && a[i] <= a[dqMin.back()])
dqMin.pop_back();
dqMin.push_back(i);
if (i >= k - 1)
cout << a[dqMin.front()] << " ";
}
cout << "\n";
// 求滑动窗口最大值
deque<int> dqMax; // 存放下标,保证对应的 a 值单调递减
for (int i = 0; i < n; i++) {
// 移除不在窗口中的下标
if (!dqMax.empty() && dqMax.front() <= i - k)
dqMax.pop_front();
// 移除所有比当前数 a[i] 小的下标
while (!dqMax.empty() && a[i] >= a[dqMax.back()])
dqMax.pop_back();
dqMax.push_back(i);
if (i >= k - 1)
cout << a[dqMax.front()] << " ";
}
cout << "\n";
return 0;
}
KMP
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P在字符串 S中多次作为子串出现。
求出模式串 P在字符串 S中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0开始计数),整数之间用空格隔开。
数据范围
1≤N≤1e5
1≤M≤1e6
输入样例:
3
aba
5
ababa
输出样例:
0 2
1.串的普通算法BF
BF算法图示过程(返回匹配成功的位置)
思想:
从主串的第pos个字符开始匹配和模式串中第一个字符串开始比较。
(1)如果相等:继续比后续字符,i++,j++;
(2)如果不相等,从主串的下一个字符和模式串 的第一个字符相比较。
任何求主串的下一个字符的位置?
方法一:设置一个变量k,在主串未开始时,领k=i+1(主串的下一个位置),每当匹配失败,另i=j,即可。
int bf(char s[],char t[],int pos)
{
int i=pos,j=1;//从主串的第pos个字符,和模式串第一个字符比较
while(i<=s.length&&j<=t.length)
{
int k=i+1; //让k等于i的下一个位置
if(s[i]==t[j]) //匹配成功,继续比较下一个位置
{
++i;
++j;
}
else //匹配失败
{
i=k;
j=1;
}
}
if(j>T.length) return i-T.length;//如果j大于模式串的长度,说明匹配成功
else return 0; //匹配失败
}
方法二:找出每次失败i和j的关系。
则下一个位置是i-j+2.
int BF(char s[],char t[],int pos)
{
int i=pos,j=1;
while(i<=s.length&&j<t.length)
{
if(s[i]==s[j])
{
++i;
++j;
}
else
{
i=i-j+2;
j=1;
}
}
if(j>t.length) return i-t.length;
else return 0;
}
2.KMP算法
特点:在匹配过程中,不需要回溯主串的指针i,时间复杂度为O(m+n)
思路:
则我们可知next数组的含义,next[i]表示:以i结尾的后缀和从1开始模式串的前缀相等,且相等最大 。
假设我们已知next数组,则模式匹配如下:
思想
主串的第pos个字符和模式串的第一个字符串进行比较
(1).相等:继续比较后继字符 i++,j++。
(2).不相等:主串的位置不变和模式串的第next[j]字符比较,j=next[j]。
下面展示一个代码:
int KMP(char s[],char t[],int pos)
{
int i=pos,j=1;
while(i<=s.length&&j<=t.length)
{
if(j==0||s[i]==t[j]) //j==0表示当前比较的是模式串的首字符且不匹配,应从主串的后一个位置继续匹配;s[i]==t[j]表示匹配成功,继续匹配。
{
++i;
++j;
}
else j=next[j];
}
if(j>t.length) return i-t.length;
else return 0;
}
求KMP的next指针的值
(1)如果t[j]==t[next[j]],则next[j+1]=next[j]+1.
(2)如果t[j]!=t[next[j]],判断t[j]和t[next[…next[j]…]],重复 过程(1),直到相等,退到0时,表示不存在,next[j+1]=1.
换句话说,要求next[j],需要判断t[j-1]和t[next[j-1]].
void get_next(char t[],int next[])
{
int j=1,k=0;
next[1]=0;
while(j<t.length)
{
if(k==0||t[j]==t[k])//k为0,或者找到时,next[j+1]=k。
{
++j;
++k;
next[j]=k;
}
else k=next[k];
}
}
KMP的nextval值
思想:
当s[i]和t[j]比较后,发现两者不相等时,但t[j]和t[k]相等,那就意味着s[i]和t[k]不需要进行额外的比较,因此j的位置的nextval值修改为k位置的nextval值,当s[i]和t[j]比较后,发现两者不相等,发现t[j]和t[k]也不相等,因此j位置的nextval值仍是k,即nextval[j]=next[j].
已知next[j],应如下修改nextval值
k=next[j];
if(t[j]==t[k]) nextval[j]=next[k];
else nextval[j]=next[j];
例如:求aaaab的nextval值。
如果t[j]==t[next[j]],nextval[j]=nextval[next[j]]
否则nextval[j]=next[j].
void get_nextval(chat t[],int next[],int nextval[])
{
int j=2,k=0;
get_next(t,next);
nextval[1]=0;
while(j<=t.length())
{
k=next[j];
if(t[j]==t[k]) nextval[j]=nextval[j];
else nextval[j]=next[j];
}
}
匹配过程和next的匹配过程类似。
AC代码
#include <iostream>
using namespace std;
const int N = 100100, M = 1000010;
int n, m; // n:模式串 p 的长度,m:主串 s 的长度
int ne[N]; // next 数组(又称为部分匹配表),用于记录模式串 p 中每个位置的最长相等前后缀长度
char s[M], p[N]; // s:主串,p:模式串
// 注意:这里均采用 1-indexing,即字符串从下标 1 开始存储
// 求模式串 p 的 next 数组,也就是部分匹配表
void get_next() {
// i 从 2 开始,因为位置 1 的 next 值通常为 0(空串没有前后缀匹配)
// j 表示当前匹配到的位置(即 p[1...j] 是 p[1...i-1] 的后缀,同时也是前缀)
for (int i = 2, j = 0; i <= n; i++) {
// 如果 p[i]与 p[j+1]不匹配,就回退 j 到 ne[j],直到找到合适的 j 或者 j 回退到 0
while (j && p[i] != p[j + 1])
j = ne[j]; // 这里利用已经计算好的部分匹配信息,将 j 回退到较小的匹配值
// 如果 p[i]与 p[j+1]匹配,则 j 向前扩展一位
if (p[i] == p[j + 1])
j++;
// 将当前位置 i 的 next 值设为 j,即 p[1...j]为 p[1...i] 的最长相等前后缀
ne[i] = j;
}
}
// 利用 KMP 算法在主串 s 中查找模式串 p 出现的位置
void kmp() {
// i:遍历主串 s,j:当前匹配模式串 p 的位置
for (int i = 1, j = 0; i <= m; i++) {
// 当 j > 0 且当前字符 s[i] 与 p[j+1]不匹配时,
// 通过 next 数组将 j 回退到较小的匹配状态(即继续尝试匹配)
while (j && s[i] != p[j + 1])
j = ne[j]; // 回退至上一个可能的匹配位置
// 如果 s[i] 与 p[j+1]匹配,则 j 向前扩展一位
if (s[i] == p[j + 1])
j++;
// 当 j 达到模式串长度 n 时,说明找到了一个完整匹配
if (j == n) {
// 输出匹配位置,注意这里输出的是 i - n,
// 因为 i 表示匹配结束的位置,i - n 即为匹配起始位置(以 1 为下标时)
printf("%d ", i - n);
// 继续查找下一个匹配,将 j 回退到上一个可能继续匹配的位置
j = ne[j];
}
}
}
int main() {
// 输入格式:首先输入模式串长度 n,
// 接着输入模式串 p(从 p+1 开始存储,即 p[1] 为模式串的第一个字符),
// 然后输入主串长度 m,接着输入主串 s(同样从 s+1 开始存储)。
cin >> n >> (p + 1) >> m >> (s + 1);
// 预处理模式串,求出部分匹配表
get_next();
// 执行 KMP 算法,查找模式串在主串中所有的出现位置
kmp();
return 0;
}
并查集
(1)朴素并查集:
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
(2)维护size的并查集:
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
(3)维护到祖宗节点距离的并查集:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
4.1质数
4.1.1试除法判定质数
给定 n个正整数 ai,判定每个数是否是质数。
输入格式
第一行包含整数 n。
接下来 n行,每行包含一个正整数 ai。
输出格式
共 n行,其中第 i行输出第 i个正整数 ai是否为质数,是则输出 Yes,否则输出 No。
数据范围
1≤n≤100,
1≤ai≤2^31−1
输入样例:
2
2
6
输出样例:
Yes
No
用试除法判断一个数n是不是质数的时间复杂度为O(sqrt(n)).
#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
bool isprime(int n)
{
if(n<2) return false;
for(int i=2;i<=n/i;i++)
{
if(n%i==0) return false;
}
return true;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
int n;
scanf("%d",&n);
if(isprime(n)) printf("Yes\n");
else printf("No\n");
}
return 0;
}
4.1.2分解质因数
给定 n个正整数 ai,将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。
输入格式
第一行包含整数 n。
接下来 n行,每行包含一个正整数 ai。
输出格式
对于每个正整数 ai,按照从小到大的顺序输出其分解质因数后,每个质因数的底数和指数,每个底数和指数占一行。
每个正整数的质因数全部输出完毕后,输出一个空行。
数据范围
1≤n≤100,
2≤ai≤2×1e9
输入样例:
2
6
8
输出样例:
2 1
3 1
2 3
#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
int main()
{
int n;
cin>>n;
while(n--)
{
int a;
cin>>a;
for(int i=2;i<=a/i;i++)
{
if(a%i==0)
{
int s=0;
while(a%i==0)
{
a/=i;
s++;
}
cout<<i<<" "<<s<<endl;
}
}
if(a>1) cout<<a<<" "<<1<<endl;
cout<<endl;
}
}
4.1.3筛质数
给定一个正整数 n,请你求出 1∼n 中质数的个数。
输入格式
共一行,包含整数 n。
输出格式
共一行,包含一个整数,表示1∼n 中质数的个数。
数据范围
1≤n≤1e6
输入样例:
8
输出样例:
4
质数定理:1~n中有近似n/lnn个质数(粗略计算)
当n=1e6,线性筛法和埃氏筛法时间近乎一样
当n=1e7,线性筛法比埃氏筛法快一倍
st[]数组标记合数
(1)朴素筛法O(nlogn)
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
int primes[N],cnt;
bool st[N];//标记是否被筛过
void get_primes(int n)
{
for(int i=2;i<=n;i++)
{
if(!st[i])
{
primes[cnt++]=i;
}
for(int j=i+i;j<=n;j+=i) st[j]=true;//把质数的倍数筛掉,质数的倍数一定是合数
}
}
int main()
{
int n;
cin>>n;
get_primes(n);
cout<<cnt<<endl;
return 0;
}
(2)埃氏筛法O(nloglogn)近乎O(n)
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
int primes[N],cnt;
bool st[N];//标记是否被筛过
void get_primes(int n)
{
for(int i=2;i<=n;i++)
{
if(!st[i])
{
primes[cnt++]=i;
for(int j=i+i;j<=n;j+=i) st[j]=true;
}
}
}
int main()
{
int n;
cin>>n;
get_primes(n);
cout<<cnt<<endl;
return 0;
}
(3)线性筛法
线性筛法
思路:每个合数,只会被它的最小质因子筛掉.
void get_primes(){
//外层从2~n迭代,因为这毕竟算的是1~n中质数的个数,而不是某个数是不是质数的判定
for(int i=2;i<=n;i++){
if(!st[i]) primes[cnt++]=i;
for(int j=0;primes[j]<=n/i;j++){//primes[j]<=n/i:变形一下得到——primes[j]*i<=n,把大于n的合数都筛了就
//没啥意义了
st[primes[j]*i]=true;//用最小质因子去筛合数
/*(1)当i%primes[j]!=0时,说明此时遍历到的primes[j]不是i的质因子,那么只可能是此时的primes[j]<i的 最小质因子,所以primes[j]*i的最小质因子就是primes[j];
(2)当有i%primes[j]==0时,说明i的最小质因子是primes[j],因此primes[j]*i的最小质因子也就应该是
prime[j],之后接着用st[primes[j+1]*i]=true去筛合数时,就不是用最小质因子去更新了,因为i有
最小质因子primes[j]<primes[j+1],此时的primes[j+1]不是primes[j+1]*i的最小质因子,此就 应该退出循环,避免之后重复进行筛选。*/
if(i%primes[j]==0) break;
}
}
}
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
int primes[N],cnt;
bool st[N];//标记是否被筛过
void get_primes(int n)
{
for(int i=2;i<=n;i++)
{
if(!st[i]) primes[cnt++]=i;
for(int j=0;primes[j]<=n/i;j++)/*从小到大枚举所有的质数,primes[j]*i<=n保证了要筛的合数
在n的范围内*/
{
st[primes[j]*i]=true;//每次把当前质数和i的乘积筛掉,也就是筛掉一个合数
if(i%primes[j]==0) break;//当这一语句执行,primes[j]一定是i的最小质因子
}
}
}
int main()
{
int n;
cin>>n;
get_primes(n);
cout<<cnt<<endl;
return 0;
}
4.2约数
4.2.1 试除法求约数
给定 n个正整数 ai,对于每个整数 ai,请你按照从小到大的顺序输出它的所有约数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出共 n 行,其中第 i 行输出第 i 个整数 ai 的所有约数。
数据范围
1≤n≤100,
2≤ai≤2×1e9
输入样例:
2
6
8
输出样例:
1 2 3 6
1 2 4 8
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int n;
void get_divisors(int n)
{
vector<int> res;
for (int i = 1; i <= n / i; i++) {
if (n % i == 0) {
res.push_back(i);
if (i != n / i) { // 避免 i==n/i, 重复放入 (n是完全平方数
res.push_back(n / i);
}
}
}
sort(res.begin(), res.end());
for (auto item : res) {
cout << item << " ";
}
puts("");
}
int main()
{
cin >> n;
while (n--) {
int x;
cin >> x;
get_divisors(x);
}
return 0;
}
4.2.2约数个数
给定 n 个正整数 ai,请你输出这些数的乘积的约数个数,答案对 1e9+7 取模。
输入格式
第一行包含整数n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出一个整数,表示所给正整数的乘积的约数个数,答案需对1e9+7 取模。
数据范围
1≤n≤100,
1≤ai≤2×1e9
输入样例:
3
2
6
8
输出样例:
12
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int mod=1e9+7;
int main()
{
int n,x;
LL ans=1;
unordered_map<int,int> hash;
cin>>n;
while(n--)
{
cin>>x;
for(int i=2;i<=x/i;i++)
{
while(x%i==0)
{
x/=i;
hash[i]++;
}
}
if(x>1) hash[x]++;
}
for(auto i:hash) ans=ans*(i.second+1)%mod;
cout<<ans;
return 0;
}
4.2.3约数之和
给定 n 个正整数 ai,请你输出这些数的乘积的约数之和,答案对 1e9+7 取模。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出一个整数,表示所给正整数的乘积的约数之和,答案需对 1e9+7 取模。
数据范围
1≤n≤100,
1≤ai≤2×1e9
输入样例:
3
2
6
8
输出样例:
252
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;
while (n -- )
{
int x;
cin >> x;
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0)
{
x /= i;
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;
}
LL res = 1;
for (auto p : primes)
{
LL a = p.first, b = p.second;
LL t = 1;
while (b -- ) t = (t * a + 1) % mod;
res = res * t % mod;
}
cout << res << endl;
return 0;
}
4.2.4最大公约数
给定 n 对正整数ai,bi,请你求出每对数的最大公约数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数对 ai,bi。
输出格式
输出共 n 行,每行输出一个整数对的最大公约数。
数据范围
1≤n≤1e5,
1≤ai,bi≤2×1e9
输入样例:
2
3 6
4 6
输出样例:
3
2
#include<bits/stdc++.h>
using namespace std;
int gcd(int a,int b)
{
return b ? gcd(b,a%b) : a;
}
int main()
{
int n;
scanf("%d",&n);
while(n--)
{
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",gcd(a,b));
}
return 0;
}
int gcd(int a,int b){
if(b > 0) return gcd(b,a % b); //如果b大于0,那么继续除
else return a; //否则直接返回a
}
进制转换模板
#include <iostream>
using namespace std;
void toTridecimal(int a) {
if (a >= 13) toTridecimal(a / 13);
int remainder = a % 13;
if (remainder < 10) cout << remainder;
else cout << char('a' + remainder - 10);
}
int main() {
int a;
cin >> a;
if (a == 0) cout << 0; // 特殊情况处理,当输入为0时直接输出0
else toTridecimal(a);
return 0;
}
堆
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;
// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
堆排序
输入一个长度为 n的整数数列,从小到大输出前 m小的数。
输入格式
第一行包含整数 n和 m。
第二行包含 n个整数,表示整数数列。
输出格式
共一行,包含 m个整数,表示整数数列中前 m小的数。
数据范围
1≤m≤n≤1e5,
1≤数列中元素≤1e9
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
一、堆的基本概念
堆:是一个完全二叉树。
堆分成两类,小根堆和大根堆。
小根堆:父节点小于等于左右孩子节点;
大根堆:父节点大于等于左右孩子节点。
STL里面的堆又称为优先队列;
如何手写一个堆?
本篇文章以小根堆为例,实现堆的一些基本的操作。
我们用一维数组来维护一个堆,规定数组的下标从1开始,每个下标的左右儿子分别为2*x,2*x+1;
我们先讲述堆中两个最基本的操作down(x),up(x)两个操作。
down(x),如果我们修改堆某个节点或者删除某个节点 ,我们就需要用down和up来维护我们堆中的关系,我们以小根堆为例,如果父节点变大,那么他就要往下沉,因为我们小根堆满足父节点小于等于左右儿子,同理,up恰好相反,如果父节点变小,它就要和自己的父节点比较,直到满足小根堆的定义为止。
二、堆的基本操作
那么我们就可以用down和up操作完成堆中最基本的操作:
1.插入一个数
我们插入一个数一般是插入到堆中最后一个数的后面再进行up操作。
heap[++size]=x,up(size);
2.求集合当中的最小值
因为是小根堆,我们堆顶元素是最小值。
heap[1];
3.删除最小值
我们需要删除堆顶元素,都是如果直接删除堆顶元素的话,会很麻烦,我们可以用最后一个元素来覆盖堆顶元素,如何进行down(1)操作。
heap[1]=heap[size];size–;down(1);
4.删除任意一个值
我们类似于删除堆顶元素的操作,我们先用最后一个元素的值覆盖删除元素的值,因为我们不知道覆盖后的元素是变大还是变小了,所有我们需要判断是执行up还是down。
int t\=heap[k];
heap[k]\=heap[size];
size--;
if(heap[k]\>t) down(k);
else up(k);
当然我们可以简化:
heap[k]\=heap[size];
size--;
down(k);
up(k);
5.修改任意一个元素
heap[k]\=x;
down(k);
up(k);
AC代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int h[N],siz;
int n,m;
void down(int u)
{
int t=u;//t存储3个节点中的最小值,开始时假设最小值为父节点
if(2*u<=siz&&h[2*u]<h[t]) t=2*u;//和左儿子比较
if(2*u+1<=siz&&h[2*u+1]<h[t]) t=2*u+1;//和右儿子比较
if(t!=u)
{
swap(h[t],h[u]);
down(t);
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>h[i];
siz=n;
for(int i=n/2;i;i--) down(i);
while(m--)
{
cout<<h[1]<<" ";
h[1]=h[siz];
siz--;
down(1);
}
return 0;
}
拓扑排序
给定一个 n个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y之前,则称 A是该图的一个拓扑序列。
输入格式
第一行包含两个整数 n和 m。
接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。
输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。
否则输出 −1。
数据范围
1≤n,m≤1e5
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3
思路:
AC代码
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
//邻接表表示方法
int h[N],e[N*2],ne[N*2],idx;
int d[N*2];
int q[N*2];//定义一个队列
int n,m;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
bool topsort()
{
int hh=0,tt=-1;
for(int i=1;i<=n;i++)
if(!d[i]) q[++tt]=i;//将入度为0的点入队
while(hh<=tt)
{
auto t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
d[j]--;
if(d[j]==0) q[++tt]=j;
}
}
return tt==n-1;//如果队列里面有n个点,则存在拓扑序列,否则有环,不存在拓扑序列
}
int main()
{
memset(h,-1,sizeof h);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
add(a,b);
d[b]++;//b的入度++
}
if(topsort())
{
for(int i=0;i<n;i++)
cout<<q[i]<<" ";
puts("");
}
else puts("-1");
return 0;
}
拓扑排序stl实现
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
using namespace std;
const int N=1e6+10;
// 邻接表存储图
int h[N], e[N*2], ne[N*2], idx; // h: 头节点,e: 目标点,ne: 下一条边,idx: 记录边序号
int d[N*2]; // 记录入度
queue<int> q;
int n, m; // 节点数,边数
// 添加边 a -> b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++; // 建立邻接表
}
// 拓扑排序(Kahn 算法)
bool topsort()
{
for(int i = 1; i <= n; i++)
if(!d[i]) q.push(i); // 先将入度为 0 的点入队
int cnt = 0; // 统计拓扑序列中的节点数
while(!q.empty())
{
int t = q.front(); q.pop(); // 取出队头
for(int i = h[t]; i != -1; i = ne[i]) // 遍历 t 的所有出边
{
int j = e[i]; // 目标节点
d[j]--; // 入度减少
if(d[j] == 0) q.push(j); // 入度变 0,入队
}
cnt++; // 统计遍历的点数
}
return cnt == n; // 如果遍历所有点,说明无环,否则有环
}
int main()
{
memset(h, -1, sizeof h); // 初始化邻接表(所有点无边)
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
add(a, b); // 添加有向边 a -> b
d[b]++; // b 的入度 +1
}
if(topsort()) puts("1"); // 无环
else puts("-1"); // 有环
return 0;
}
树和图的一些预备知识
树与图的存储
树是一种特殊的图,与图的存储方式相同。
对于无向图中的边ab,存储两条有向边a->b, b->a。
因此我们可以只考虑有向图的存储。
n:点数,m:边数
稀疏图:如果m和n是一个级别的,用邻接表。
稠密图:如果m和n^2是一个级别的,用邻接矩阵。
(1) 邻接矩阵:g[a][b] 存储边a->b,先初始化g位正无穷
memset(g,0x3f,sizeof g);
g[a][b]=c;
(2) 邻接表:
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 初始化
idx = 0;
memset(h, -1, sizeof h);//初始化表头
(1) 深度优先遍历
时间复杂度 O(n+m) ,n表示点数,m表示边数.
int dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
(2) 宽度优先遍历
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
3.3树的深度优先遍历
树和图的深度优先遍历的模板:
// 需要标记数组st[N], 遍历节点的每个相邻的便
void dfs(int u) {
st[u] = true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!st[j]) {
dfs(j);
}
}
}
3.3.1树的重心
给定一颗树,树中包含 n个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数 n,表示树的结点数。
接下来 n−1行,每行包含两个整数 a和 b,表示点 a和点 b之间存在一条边。
输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。
数据范围
1≤n≤1e5
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4
每次算出他下面的size和n-size进行比较即可。
AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10; //数据范围是10的5次方
const int M = 2 * N; //以有向图的格式存储无向图,所以每个节点至多对应2n-2条边
int h[N]; //邻接表存储树,有n个节点,所以需要n个队列头节点
int e[M]; //存储元素
int ne[M]; //存储列表的next值
int idx; //单链表指针
int n; //题目所给的输入,n个节点
int ans = N; //表示重心的所有的子树中,最大的子树的结点数目
bool st[N]; //记录节点是否被访问过,访问过则标记为true
//a所对应的单链表中插入b a作为根
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// dfs 框架
/*
void dfs(int u){
st[u]=true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(!st[j]) {
dfs(j);
}
}
}
*/
//返回以u为根的子树中节点的个数,包括u节点
int dfs(int u) {
int res = 0; //存储 删掉某个节点之后,最大的连通子图节点数
st[u] = true; //标记访问过u节点
int sum = 1; //存储 以u为根的树 的节点数, 包括u,如图中的4号节点
//访问u的每个子节点
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
//因为每个节点的编号都是不一样的,所以 用编号为下标 来标记是否被访问过
if (!st[j]) {
int s = dfs(j); // u节点的单棵子树节点数 如图中的size值
res = max(res, s); // 记录最大联通子图的节点数
sum += s; //以j为根的树 的节点数
}
}
//n-sum 如图中的n-size值,不包括根节点4;
res = max(res, n - sum); // 选择u节点为重心,最大的 连通子图节点数
ans = min(res, ans); //遍历过的假设重心中,最小的最大联通子图的 节点数
return sum;
}
int main() {
memset(h, -1, sizeof h); //初始化h数组 -1表示尾节点
cin >> n; //表示树的结点数
// 题目接下来会输入,n-1行数据,
// 树中是不存在环的,对于有n个节点的树,必定是n-1条边
for (int i = 0; i < n - 1; i++) {
int a, b;
cin >> a >> b;
add(a, b), add(b, a); //无向图
}
dfs(1); //可以任意选定一个节点开始 u<=n
cout << ans << endl;
return 0;
}
3.4树的广度优先遍历
3.4.1图中点的层次
给定一个 n个点 m条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1号点到 n号点的最短距离,如果从 1号点无法走到 n号点,输出 −1。
输入格式
第一行包含两个整数 n和 m。
接下来 m行,每行包含两个整数 a和 b,表示存在一条从 a走到 b的长度为 1 的边。
输出格式
输出一个整数,表示 1号点到 n号点的最短距离。
数据范围
1≤n,m≤1e5
输入样例:
4 5
1 2
2 3
3 4
1 3
1 4
输出样例:
1
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
//邻接表表示方法
int h[N],e[N*2],ne[N*2],idx;
int n,m;
int d[N];//标记距离1号点的最短距离
bool st[N];//标记访问标志
int q[N];//定义一个队列
//从a->b连接一条边
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int bfs()
{
memset(d,-1,sizeof d);
int hh=0,tt=-1;
d[1]=0;
q[++tt]=1;//从1号点开始搜索
st[1]=true;
while(hh<=tt)
{
int t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i])//访问该点的邻接点
{
int j=e[i];
if(!st[j])
{
d[j]=d[t]+1;
q[++tt]=j;
st[j]=true;
}
}
}
return d[n];
}
int main()
{
memset(h,-1,sizeof h);//初始化邻接表头
scanf("%d%d",&n,&m);
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
}
printf("%d\n",bfs());
return 0;
}
高精度算法
性质:数组或者容器从低位往高位依次存储大整数,方便进位。
1.5.1高精度加法
给定两个正整数(不含前导 0),计算它们的和。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的和。
数据范围
1≤整数长度≤100000
输入样例:
12
23
输出样例:
35
思路:
模拟人工加法。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
using namespace std;
// 高精度加法,A 和 B 代表两个大数(低位在前)
vector<int> sum(vector<int> &A,vector<int> &B)
{
vector<int> C;
int k=0; // 进位
for(int i=0;i<max(A.size(),B.size());i++)
{
if(i<A.size()) k+=A[i];
if(i<B.size()) k+=B[i];
C.push_back(k%10); // 取当前位
k/=10; // 计算进位
}
if(k) C.push_back(1); // 处理最高位可能的进位
return C;
}
int main()
{
string a,b;
vector<int> A,B;
cin>>a>>b;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0'); // 逆序存储
for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0'); // 逆序存储
vector<int> C=sum(A,B);
for(int i=C.size()-1;i>=0;i--) cout<<C[i]; // 逆序输出
return 0;
}
1.5.2高精度减法
给定两个正整数(不含前导 0),计算它们的差,计算结果可能为负数。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的差。
数据范围
1≤整数长度≤105
输入样例:
32
11
输出样例:
21
思路:
模拟人工减法。
#include <bits/stdc++.h>
using namespace std;
vector<int> A,B;
// 比较两个高精度数的大小,A >= B 返回 true,否则返回 false
bool cmp(vector<int> &A,vector<int> &B){
if(A.size()!=B.size()) return A.size()>B.size(); // 长度不同,长度长的数大
else{
for(int i=A.size()-1;i>=0;i--){ // 长度相同,从高位开始比较
if(A[i]!=B[i]) return A[i]>B[i];
}
}
return 1; // 数值相等,视为 A >= B
}
// 高精度减法,计算 A - B,保证 A >= B
vector<int> sub(vector<int> &A,vector<int> &B){
int k=0; // 进位标记(上一位借走的位数)
vector<int> C;
for(int i=0;i<A.size();i++){
int t=A[i]-k;
if(i<B.size()) t-=B[i]; // 若 B 还有数,则减去 B[i]
if(t<0) t+=10,k=1; // 借位处理
else k=0;
C.push_back(t);
}
while(C.size()>1&&C.back()==0) C.pop_back(); // 去除前导零
return C;
}
int main(){
string a,b;
cin>>a>>b;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0'); // 逆序存储
for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0'); // 逆序存储
vector<int> C;
if(cmp(A,B)) C=sub(A,B); // A >= B,结果为非负
else C=sub(B,A),cout<<"-"; // A < B,输出负号
for(int i=C.size()-1;i>=0;i--) cout<<C[i]; // 逆序输出
return 0;
}
1.5.3高精度乘法
给定两个非负整数(不含前导 0)A 和 B,请你计算 A×B 的值。
输入格式
共两行,第一行包含整数 A,第二行包含整数 B。
输出格式
共一行,包含 A×B 的值。
数据范围
1≤A的长度≤100000,
0≤B≤10000
输入样例:
2
3
输出样例:
6
高精度x低精度
// 高精度 x 低精度
#include<bits/stdc++.h>
#include<vector>
using namespace std;
// 计算 A * b,A 是高精度数(低位在前),b 是普通整数
vector<int> mul(vector<int> &A,int b)
{
vector<int> C;
int t=0; // 进位
for(int i=0;i<A.size();i++)
{
t+=A[i]*b; // 当前位相乘加上进位
C.push_back(t%10); // 取当前位
t/=10; // 计算进位
}
while(t) // 处理剩余的进位
{
C.push_back(t%10);
t/=10;
}
while(C.size()>1&&C.back()==0) C.pop_back(); // 去除前导零
return C;
}
int main()
{
string a;
int b;
cin>>a>>b;
vector<int> A;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0'); // 逆序存储
auto C=mul(A,b);
for(int i=C.size()-1;i>=0;i--) cout<<C[i]; // 逆序输出
return 0;
}
高精度x高精度
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 1e5+10;
int A[N],B[N],C[N];
int la,lb,lc;
// 高精度乘法:计算 A * B,结果存入 C
void mul(int A[],int B[],int C[])
{
for(int i=0;i<la;i++)
for(int j=0;j<lb;j++)
{
C[i+j]+=A[i]*B[j]; // 乘法累加
C[i+j+1]+=C[i+j]/10; // 处理进位
C[i+j]%=10; // 保留当前位
}
while(lc&&C[lc]==0) lc--; // 去除前导零
}
int main()
{
string a,b;
cin>>a>>b;
la=a.size();
lb=b.size();
lc=la+lb+10; // 乘积最多占 la + lb 位
for(int i=a.size()-1;i>=0;i--) A[la-i-1]=a[i]-'0'; // 逆序存储
for(int i=b.size()-1;i>=0;i--) B[lb-i-1]=b[i]-'0'; // 逆序存储
mul(A,B,C);
for(int i=lc;i>=0;i--) cout<<C[i]; // 逆序输出
return 0;
}
1.5.4高精度除法
给定两个非负整数(不含前导 0)A,B,请你计算 A/B的商和余数。
输入格式
共两行,第一行包含整数 A,第二行包含整数 B。
输出格式
共两行,第一行输出所求的商,第二行输出所求余数。
数据范围
1≤A的长度≤100000,
1≤B≤10000,
B 一定不为 00
输入样例:
7
2
输出样例:
3
1
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// 高精度除法:计算 A / B,返回商 C,余数存入 r
vector<int> div(vector<int> &A, int B, int &r)
{
vector<int> C;
for(int i=0; i<A.size(); i++)
{
r = r * 10 + A[i]; // 余数左移一位,加上当前位
C.push_back(r / B); // 计算当前位的商
r %= B; // 更新余数
}
reverse(C.begin(), C.end()); // 逆序存储,调整为高位在前
while(C.size() > 1 && C.back() == 0) C.pop_back(); // 去除前导零
return C;
}
int main()
{
string a;
int B, r = 0;
cin >> a >> B;
vector<int> A;
for(int i=0; i<a.size(); i++) A.push_back(a[i] - '0'); // 转换为数字数组
auto C = div(A, B, r);
for(int i=C.size()-1; i>=0; i--) cout << C[i]; // 逆序输出商
cout << endl << r; // 输出余数
return 0;
}
1.5.5高精度阶乘
问题描述
输入一个正整数n,输出n!的值。
其中n!=1*2*3*…*n。
算法描述
n!可能很大,而计算机能表示的整数范围有限,需要使用高精度计算的方法。使用一个数组A来表示一个大整数a,A[0]表示a的个位,A[1]表示a的十位,依次类推。
将a乘以一个整数k变为将数组A的每一个元素都乘以k,请注意处理相应的进位。
首先将a设为1,然后乘2,乘3,当乘到n时,即得到了n!的值。
输入格式
输入包含一个正整数n,n<=1000。
输出格式
输出n!的准确值。
样例输入
10
样例输出
3628800
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e5+10;
int n;
int a[N];
int main()
{
scanf("%d", &n);
a[1] = 1; // 初始化阶乘结果为 1
int t = 0; // 进位
for(int i = 2; i <= n; i++) // 计算 n!
{
for(int j = 1; j <= 10000; j++) // 逐位相乘
{
int p = a[j] * i + t; // 当前位乘积加进位
a[j] = p % 10; // 仅保留个位
t = p / 10; // 计算新的进位
}
}
n = 10000;
while(a[n] == 0) n--; // 去除前导零,找到最高位
for(int i = n; i >= 1; i--) cout << a[i]; // 逆序输出结果
return 0;
}
背包问题
5.1.1 01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
思路:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1005;
int v[N*N],w[N*N]; // v[i] 表示第 i 件物品的体积, w[i] 表示第 i 件物品的价值
int f[N][N]; // f[i][j] 表示前 i 件物品在容量 j 下的最大价值
int n,m;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i]; // 读取 n 件物品的体积和价值
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=f[i-1][j]; // 不选当前物品 i,则价值等于前 i-1 件物品的最优解
if(j>=v[i]) // 只有当容量 j 能放下物品 i 时才考虑选它
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]); // 选与不选取较优解
}
}
cout<<f[n][m]<<endl; // 输出前 n 件物品在容量 m 下的最大价值
return 0;
}
5.1.2 完全背包问题
有 N 种物品和一个容量是 V的背包,每种物品都有无限件可用。
第 i种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
思路:
完全背包是求前缀的最大值,第一次求前1项的max,第二次求前2项的max,…
#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N]; // f[i][j] 表示前 i 件物品在容量 j 下的最大价值
int v[N],w[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i = 1 ; i <= n ;i ++) cin>>v[i]>>w[i]; // 读取物品体积和价值
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=f[i-1][j]; // 不选当前物品 i
if(j>=v[i])
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]); // 选当前物品 i,可重复选
}
}
cout<<f[n][m]<<endl; // 输出最大价值
return 0;
}
5.1.3 多重背包问题I
有 N种物品和一个容量是 V 的背包。
第 i种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 ii 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
完全背包模型的基础上加了一个限制。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1010;
int v[N],w[N],s[N]; // v[i]: 物品体积, w[i]: 物品价值, s[i]: 物品数量限制
int f[N][N]; // f[i][j] 表示前 i 件物品在容量 j 下的最大价值
int n,m;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i]; // 读取每件物品的体积、价值和数量限制
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
f[i][j]=f[i-1][j]; // 不选当前物品 i
for(int k=1;k<=s[i];k++) // 枚举选取 k 件物品 i
if(j>=k*v[i])
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]); // 选 k 件的最优解
}
cout<<f[n][m]<<endl; // 输出最大价值
return 0;
}
按照01背包进行优化为一维:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1100;
int n,m,v,w,s;
int f[N];
int main()
{
scanf("%d%d", &n,&m);
for(int i=1;i<=n;i++)
{
cin>>v>>w>>s;
for(int j=m;j>=0;j--) // 逆序遍历容量,保证物品不会被重复计算
for(int k=0;k<=s&&k*v<=j;k++) // 枚举当前物品的选取数量 k
f[j]=max(f[j],f[j-k*v]+k*w); // 取选与不选的最优解
}
cout<<f[m]<<endl; // 输出最大价值
return 0;
}
5.1.4多重背包问题 II
有 N种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000
提示:
本题考查多重背包的二进制优化方法。
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
我们可以用二进制对其进行优化。
#include<iostream>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N]; // 存储物品的体积和价值(经过二进制拆分)
int f[M]; // 01 背包的动态规划数组
int main()
{
cin >> n >> m;
int cnt = 0; // 拆分后的物品数量
for(int i = 1; i <= n; i++)
{
int a, b, s;
cin >> a >> b >> s;
int k = 1; // 进行二进制拆分的当前数量
while(k <= s)
{
cnt++; // 增加新物品
v[cnt] = a * k; // 拆分出的物品体积
w[cnt] = b * k; // 拆分出的物品价值
s -= k; // 剩余数量减少
k *= 2; // 采用二进制倍增法
}
// 处理剩余的部分
if(s > 0)
{
cnt++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt; // 物品数量变为拆分后的总数
// 01 背包一维优化
for(int i = 1; i <= n; i++)
for(int j = m; j >= v[i]; j--) // 逆序遍历,避免状态污染
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl; // 输出最大价值
return 0;
}