文章目录
- 【技巧】
- 【基础算法】
- 【数据结构】
- 【C++ STL简介】
- 【搜索与图论】
- 【数学知识】
- 【由数据范围反推算法复杂度以及算法内容】
- 【动态规划】
- 【贪心】【先选最优 -- sort】
【技巧】
① #include< bits/stdc++.h > 比特/stdc++.h 万能头文件
#include< algorithm >库的使用
find(a,a+n,elem)
reverse(a,a+n)
sort(a,a+n,greater< int >() );
sort(a ,a+n ,greater< int >() );// 降序 , 不加就默认升序 **
a从起始第一位开始,a+1就是从第二位开始了
vector c(v.size());
// 拷贝
copy(v.begin(), v.end(), c.begin());
// 指定范围换值
replace(c.begin(), c.end(), 2, 666);
// 交换两个容器中的内容
swap(c, b);
bool cmp1(int a,int b)//升序
{
return a<b;
}
bool cmp2(int a,int b)//降序
{
return a>b;
}
// 查找指定元素 没有返回 end
find(v.begin(), v.end(), 9);
②卡特兰数**- 【尝试法】 基本思想 : 01整数,括号匹配
下面解为 Cat(n) = C(2 * n,n) / (n + 1)
- 出栈次序
一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列?
2.01序列
给出一个n,要求一个长度为2n的01序列,使得序列的任意前缀中1的个数不少于0的个数, 有多少个不同的01序列?
本质就是出栈次序
3.+1’‘-1’序列 ,要求序列非负- 括号序列
n对括号有多少种匹配方式?- 二叉树计数
有n个节点构成的二叉树(非叶子节点都有2个儿子),共有多少种情形?
有n+1个叶子的二叉树的个数?- 单调路径
从 (0,0) – > (n,n) 不经过y = x 的走法总数
③格式 printf(“%02d,%02d,%02d”,hh,mm,ss);
④结构体数组 edges[N]
bool operator < (const &Edge W)const{
return w < W.w;
}
⑤记忆一些模拟普通例题:
check() , 通分,全排列 , 递归 , 先打出" | * " 最后再打 " | "
ASCLL : i - ‘A’ 与 i - ‘0’
⑥2019年 |= 两个成立一个简写
目录
最终考试模板
⑦基础知识点
ASCLL: string转int a[i] - ‘0’
基础算法
#include 快排sort(a,a+a.size(),greater)
加 和 乘高精度 【统一逆序】
双指针算法
位运算
前缀和与差分
数据结构
并查集
链表与邻接表:树与图的存储
栈与队列:单调队列、单调栈
Trie字典树可以统计出现的次数 【放】
字符串前缀hash法 - 判是否相等
搜索与图论:
DFS与BFS
树与图的遍历:拓扑排序 【放?最后来】
最短路 SPFA单源 + Floyd多源
最小生成树 克鲁斯卡尔
数学知识:
线性筛法求质数
约数 【试除法】
快速幂
求组合数 --> 卡特兰数: C(2*n,n ) / (n + 1)
容斥原理
动态规划:
背包问题
线性DP
贪心 :
【基础算法】
高精度加法 【逆序存放 ,每位存t%10】
注意点:string 不能用%s ,用cin 且字符串转成整数 A.push_back(a[i] - ‘0’)
#include <iostream>
#include<algorithm>
#include<vector>
// C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> &A, vector<int> &B)//注意A中存放的是逆序的数!!! 【从个位开始加,好算】
{
if (A.size() < B.size()) return add(B, A);
vector<int> C;
int t = 0;
for (int i = 0; i < A.size(); i ++ )
{
t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
if (t) C.push_back(t); //若t != 0 ,放入,如两个数位数一样时进位1
return C;
}
/*只要先字符串!! 若给长度就先输入就是了,但没有用 */
int test_01()
{
string a,b;//输入字符串
vector<int> A,B,C;
cin >> a >> b; // 举个例子 :a = "123456" -- 对应A赋值
for(int i = a.size() - 1;i >= 0;i--) A.push_back(a[i] - '0'); //赋值A = {6,5,4,3,2,1}; 从个位开始逆序存放数值
for(int i = b.size() - 1;i >= 0;i--) B.push_back(b[i] - '0'); //(注意:最后一位对应数组下标是size() - 1)
C = add(A,B); //auto会自动判断类型 这里等价于vector<int> Dev_c++好像无法判断 ,把C先和A,B一起创建了
for(int i = C.size() - 1;i >= 0;i--) printf("%d",C[i]);
return 0;
}
高精度乘法
记忆点: i < A.size() || t 这个还有判断一次因为这次执行可能是因为满足t != 0(有进位超过A的位数)
但仍需满足 if (i < A.size()) ,放入C.push_back(t % 10); 最后去除前导0
while (C.size() > 1 && C.back() == 0) C.pop_back();
// C = A * b, A >= 0, b >= 0
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
int t = 0;
for (int i = 0; i < A.size() || t; i ++ ) //条件 加个 t判断是否进位超过A位数继续加入C 【 A没有循环完 && t!=0 就继续 】
{
if (i < A.size()) t += A[i] * b;
C.push_back(t % 10);
t /= 10;
}
while (C.size() > 1 && C.back() == 0) C.pop_back(); //【清除前导0】 C的最后一位是 乘积的第一位
return C;
}
int test_03()
{
string a;
int b;
cin >> a >> b;
vector<int> A,C;
for(int i = a.size() - 1;i >= 0;i--) A.push_back(a[i] - '0');//ASCLL转真正整数
C = mul(A,b);
for(int i = C.size() - 1;i >= 0;i--) printf("%d",C[i]);
return 0;
}
一维前缀和数组 【从1开始<=n存放和遍历,a[i] = ∑b[1 ~ i] (a是b的前缀和数组)】
前缀和数组下标从1开始 ,公式s[i] = s[i - 1] + a[i]
前缀和数组作用:O(1)算出[l,r]区间的值的和 s[r] - s[l - 1]
下标从1开始原因 : 比如∑a[1~r],需写成 s[r] - s[0] (若从0开始,则如这样的边界求和,第一个下标要变成-1,还要加个if判断)
减少空间优化法,但不保存原数组
for(int i = 1;i <= n;i++) scanf(“%d”,&s[i]); //减少空间优化法,但不保存原数组
for(int i = 1;i <= n;i++) s[i] += s[i - 1]; //前缀和的初始化
const int N = 100010; //如果修改数据只要在一个地方修改,而且0很多时就不会写错
int test_05(){
int n,m;
int a[N],s[N];
//ios::sync_with_stdio(false); //使得cin与标准输入输出失去同步,加快读取速度 ,副作用:不能用scanf了
scanf("%d%d",&n,&m);
//两行可以合并
for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
for(int i = 1;i <= n;i++) s[i] = s[i - 1] + a[i]; //前缀和的初始化
while(m--)//m次询问写法
{
int l,r;
scanf("%d%d",&l,&r);
printf("%d\n",s[r] - s[l - 1]); //区间和的计算
}
return 0;
}
796.子矩阵的和 [二维前缀和]
二维要点:读入矩阵a[i][j]
前缀和数组初始化: s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
int x1,x2,y1,y2;//题目读入顺序!!
scanf(“%d%d%d%d”,&x1,&y1,&x2,&y2);
【重要总结 :凡是数组有用到下标i-1的都从1开始定义 ! dp / hash / dfs等】
输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数n,m,q。
接下来n行,每行包含m个整数,表示整数矩阵。
接下来q行,每行包含四个整数x1, y1, x2, y2,表示一组询问。
输出格式
共q行,每行输出一个询问的结果。
数据范围
1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
-1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21
思路:
1.s[i,j]如何计算
2.(x1,x2),(x2,y2) 分别子矩阵为左上角和右下角坐标
3. sx2,y2 = s[x1,y1](大到边界) - s[x1 - 1,y2] - s[x2,y1 - 1] + s[x1 - 1, y1 -1]
(s[x2,y2]指x2*y2的矩阵 ,其他同理 ,思想:大矩阵 - 边界的矩阵(分3个部分) + 重复多减的部分)
画图推导dp s[i,j] = s[i-1,j] + s[i,j-1] - s[i-1,j-1] + a[i,j]
子矩阵左上角和右下角坐标 (x1,y1) ,(x2,y2)
s[x2][y2]指的是:从边界开始的 x2 * y2 的矩阵
子矩阵求和 == s[x2][y2] - s[x1][y2 - 1] - s[x2][y1 - 1] + s[x1][y1]
构造差分数组 b[i] = a[i] - a[i - 1]
【画图想象子矩阵的加 == 用大矩阵全加上再减去部分不属于子矩阵的】
const int N = 1010;
int n,m,q; //q次询问
int a[N][N],s[N][N];
int test_05(){
scanf("%d%d%d",&n,&m,&q);
for(int i = 1;i <= n;i++){
for(int j = 1;j <= m;j++){
scanf("%d",&a[i][j]);
}
}
for(int i = 1;i <= n;i++){
for(int j = 1;j <= m;j++){
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
}
}
while(q--)
{
int x1,x2,y1,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);//输入顺序依据题目要求 ! !!
printf("%d\n",s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]); //求子矩阵的和 (之前s数组已经加过初始化了,现在只要找到对应区间即可)
}
return 0;
}
797. 差分[a是b的前缀和数组 - 差分是前缀和的逆运算!!] 【某个区间快速删减值】
思路分析
给定a[1],a[2],…a[n]构造差分数组b[n],使得
a[i]=b[1]+b[2]+…+b[i];
要使a[L~R]+=c,等价于 b[L]+=c, b[R+1]-=c;
原理:a[1~L-1]=b[1]+b[2]+…+b[L-1];a[1 ~L-1]无影响
a[L~R]=b[1]+b[2]+…+b [ L ](此时的b[ L ]加上了c)+…;所以等价于a[L ~R]中的每个数都加上了c。
a[R+1~ N]=b[1]+b[2]+…+b[ L](此时的b[ L]加上了c)+…+b[ R+1](此时的b[ R+1]减去了c,两者抵消)+…;所以a[R+1~N]无影响。
初始化:b[n]的每个元素都为0,利用差分函数构造差分数组
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2
一维差分:
b[l]+=c;//包括l且不包括r+1的区间+c ,即a[l~r]区间
b[r+1]-=c;//【包括r+1的区间且不包括l的区间不受影响】 即[1,l-1] 和[r+1,最后]不受影响
求a[ i ] = a[ i - 1 ] + b[ i ]
易错: for(int i=1;i<=n;i++){
t+=b[i];//对差分数组求和 类似前缀和数组初始化,求一遍a[i] !!! a[i] = a[i - 1] + b[i]
cout<<t<<" ";
}
二维差分:
b[x1][y1] += c
b[x2 + 1][y1] -= c
b[x1][y2 + 1] -= c
b[x2 + 1][y2 + 1] += c
#include <iostream>
using namespace std;
int a[100005],b[100005];
void insert(int l,int r,int c){
b[l]+=c;//包括l且不包括r+1的区间+c ,即a[l~r]区间
b[r+1]-=c;//【包括r+1的区间且不包括l的区间不受影响】 即[1,l-1] 和[r+1,最后]不受影响
}
int test_06()
{
int n,m;
int l,r,t;
cin>>n>>m;
//使得a[i]=b[1]+b[2]+…+b[i];
for(int i=1;i<=n;i++){ //下标从1开始
cin>>a[i];
insert(i,i,a[i]);
}
while(m--){//m次差分操作【题目要求】
cin>>l>>r>>t;
insert(l,r,t);
}
t=0;
for(int i=1;i<=n;i++){
t+=b[i];//对差分数组求和 类似前缀和数组初始化,求一遍a[i] !!! a[i] = a[i - 1] + b[i]
cout<<t<<" ";
}
return 0;
}
整数二分算法模板 [789. 数的范围]
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;
}
快速排序算法模板 [785. 快速排序]
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
[二维差分] 【有空自己推导,始终记住a是b的前缀和数组】
给定原矩阵a[i,j],构造差分矩阵 b[i,j],使得a[][]是b[][]的二维前缀和
差分核心操作:给以(x1,y1)为左上角,(x2,y2)为右下角的子矩阵的所有数a[i,j] ,加上C 【包含这个格子的大矩阵都被影响要+C】
对于差分数组的影响:
b[x1][y1] += c;//x1,y1之前的矩阵+C
b[x1][y2+1] -= c; //旁边的矩阵不受影响
b[x2+1][y1] -= c;
b[x2+1][y2+1] += c;//x2,y2之后的大矩阵不受影响 【 ±± C == 0 】
a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j -1] + b[i][j]; //二维前缀和公式
记忆:容斥原理记 二维前缀和公式
#include<cstdio>
int n,m,q;
int a[1010][1010],b[1010][1010];
//差分操作
void insert(int x1,int y1,int x2,int y2,int c){
b[x1][y1] += c;//x1,y1之前的矩阵+C
b[x1][y2+1] -= c; //旁边的矩阵不受影响
b[x2+1][y1] -= c;
b[x2+1][y2+1] += c;//x2,y2之后的大矩阵不受影响 +--+ C == 0
}
int test_07(){
scanf("%d%d%d",&n,&m,&q);
for(int i = 1;i <= n;i++){
for(int j = 1;j <= m;j++){
scanf("%d",&a[i][j]);
}
}
for(int i = 1;i <= n;i++){
for(int j = 1;j <= m;j++){
insert(i,j,i,j,a[i][j]);
}
}
//询问
while(q--)
{
int x1,y1,x2,y2,c;
scanf("%d%d%d%d%d",&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] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j -1] + b[i][j]; //二维前缀和公式
}
}
for(int i = 1;i <= n;i++){
for(int j = 1;j <= m;j++){
printf("%d ",a[i][j]);
}
puts("");
}
return 0;
}
[双指针算法] 【例题799-800】【单调性优化】
(经典双指针算法 【优化-单调性 - 才用双指针】) 799-800 ******************************************************************
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
799. 最长连续不重复子序列 【s[a[i]] > 1,s[a[i]]- -,j++回退】
s[a[i]] ++ ;//【存值标记数组,已有就说明重复 】
while(s[a[i]] > 1) //【重复了】
{
s[a[j]] --;//此位置重复,不能走,回退
j ++;//说明不能再往左走了,往右回退 ,线性遍历
}
res = max(res , i - j + 1); // return ans = max_length[i,j]
#include<algorithm>
int test_02()
{
const int N = 100010;
int n,a[N],s[N];
cin >> n;
for(int i = 0;i < n; i++) cin >> a[i];
int res = 0;
for(int i = 0,j = 0;i < n;i++)
{
s[a[i]] ++ ;//【存值标记数组,已有就说明重复 】
while(s[a[i]] > 1) //【重复了】
{
s[a[j]] --;//此位置重复,不能走,回退
j ++;//说明不能再往左走了,往右回退 ,线性遍历
}
res = max(res , i - j + 1); // return ans = max_length[i,j]
}
cout << res << endl;
return 0;
}
800.数组元素的目标和
(经典双指针算法 优化-单调性)【唯一解才这样做才O(n) ,多解仍然为O(n2)
给定两个升序排序的有序数组A和B,以及一个目标值x。数组下标从0开始。
请你求出满足A[i] + B[j] = x的数对(i, j)。
此题A,B单调递增
for i --> n ,i = 0,j = m - 1 (开始时:A指向第一个,B指向最后一个位置)
while(j >= 0 && A[i] + B[j] > x) j–; //O(n+m)
const int N = 100010;
int n,m,x;
int a[N],b[N];
int test_01() {
scanf("%d%d%d",&n,&m,&x);
for(int i = 0; i < n; i++) scanf("%d",&a[i]);
for(int i = 0; i < m; i++) scanf("%d",&b[i]);
for(int i = 0,j = m - 1; i < n; i++) { //j从尾部开始
while(j >= 0 && a[i] + b[j] > x) j--;
if(a[i] + b[i] == x) {
printf("%d %d\n",i,j);
break;
}
}
return 0;
}
[位运算] 【lowbit代表最后一位1的数值】
n的二进制中表示第k位的变化
n = 15 = 0b(1111)
①先把第k位移到最后一位,n >> k
②看n的个位是几 : n & 1
③(①+ ②并用) 看n的第k位是几 : n >> k & 1
lowbit(x) {return x & -x;} 返回x的最后一位1的位置代表的数值
801.二进制中1的个数
题目描述
给定一个长度为n的数列,输出数组中每个数的二进制中1的个数
int lowbit(int x)//x的最后一位1的数值
{
return x & -x;
}
int test_04()
{
int n,res;
cin >> n;
while(n --) //n次询问
{
int x;
cin >> x;
int res = 0;
while(x) x -= lowbit(x),res++; //每次减去最后一位1对应的数值,统计1的个数
cout << res << " ";
}
return 0;
}
#include<cstdio>
int main() {
test_04();
return 0;
}
【数据结构】
826. 单链表 【数组模拟】
head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void insert(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}
// 将头结点删除,需要保证头结点存在
void remove()
{
head = ne[head];
}
[邻接表] 【数组模拟】
对于每个点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);
并查集 [836. 合并集合] + [837. 连通块中点的数量]
记忆find 返回x的祖宗节点p[x] , 递归 p[x] = find(p[x]);从父找到祖宗
维护路径:维护size【记录元素个数】初始化size[i] = 1;,
合并操作size[find(b)] += size[find(a)];
维护到祖宗节点距离的并查集:看具体代码…find变型
(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)的偏移量
830. 单调栈
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
单调队列 —— 模板题 AcWing 154. 滑动窗口
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
q[ ++ tt] = i;
}
841. 字符串哈希 - 字符串相等 【效率高于kmp】
记忆点:typedef unsigned long long ULL;大P = 131
char str[N] ; scanf(“%s”,str + 1) !!!从1开始存
ULL get(int l,int r)
scanf(“%d%d%s”,&n,&m,str + 1); //str从1开始存储
p[0] = 1;//P0 == 1
for(int i = 1;i <= n;i++) //初始化!!
{
p[i] = p[i - 1] * P; //注意 * 大写P
h[i] = h[i - 1] * P + str[i]; //保证str[i] != 0 就可以
}
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
str = “ABCABCDEFG”
h[0] = “A” 的hash值
h[1] = “AB” 的hash值
h[2] = “ABC” 的hash值
h[3] = “ABCD” 的hash值
①看做p进制的数
如果把 A、B、C、D看做 (1,2,3,4)p 的十进制 = 1 * p3 + 2 * p 2 + 3 * p1 + 4 * p0 mod Q
就可以把任意一个字符串映射到 0 ~ Q - 1
②【一般不能把字符映射成0 !】 如A映射成0 A为0 ,AA也是0 ,冲突! 【从1开始
③假定人品足够好,不存在冲突时 【经验值:当 p = 131 ,Q mod 264 时-----99.9%不会发生冲突 】
【h[]用 unsighed long long 存储就可以(溢出等效取模),等价于mod 264 】
能用公式算出[l~k]段子串的hash值(已知 h[1 ~ l]和h[1 ~ k]的hash值) hash[l ~ k] == h[k] - h[l] * p ^k - l + 1^ (左到右区间hash值等效计算)
【h[k] 对应–> pk-1 ~ ~ p0 h[l-1] 对应–> pl-2 ~~ p0 】
【把h[l-1]往左移,与h[k]对齐,相减等效得出子串的hash值:即以子串的第一位为最高位的区间得出的hash值 】
h[1]为最高位
字符串哈希表作用(比kmp还牛!超快速判断两个字符是否相等O(1) ,kmp可以循环节,其他均比不过hash)
【任意两个长度相等的区间的hash值相等,则两个区间的字符串相等】
字符串哈希
题目描述
给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2l1,r1,l2,r2,请你判断[l1,r1l1,r1]和[l2,r2l2,r2]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数n和m,表示字符串长度和询问次数。
第二行包含一个长度为n的字符串,字符串中只包含大小写英文字母和数字。
接下来m行,每行包含四个整数l1,r1,l2,r2l1,r1,l2,r2,表示一次询问所涉及的两个区间。
注意,字符串的位置从1开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1≤n,m ≤105
输入样例:(字符串长度 , 查询次数 ,给定区间判断是否相等)
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
typedef unsigned long long ULL; // unsigned long long等效取模 mod 2^64^
const int N = 100010,P = 131; //或1331 统一用 p = 131 记住!
int n,m;
char str[N];
ULL h[N],p[N]; //p数组存储p的多少次方的值 {p^1^,p^2^,p^3^,p^4^,p^5^};
//h[k]存前k个字符的hash值
ULL get(int l,int r) //计算区间hash值
{
return h[r] - h[l - 1] * p[r - l + 1]; //平移r-l+1相减 ,乘上大P的r-l+1次方
}
int test_03()
{
scanf("%d%d%s",&n,&m,str + 1); //str从1开始存储
p[0] = 1;//P^0^ == 1
for(int i = 1;i <= n;i++) //初始化!!
{
p[i] = p[i - 1] * P; //注意 * 大写P
h[i] = h[i - 1] * P + str[i]; //保证str[i] != 0 就可以
}
while(m--)
{
int l1,l2,r1,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);//顺序!!
if(get(l1,r1) == get(l2,r2)) puts("Yes");
else puts("No");
}
return 0;
}
【C++ STL简介】
vector, 变长数组,倍增的思想
size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end()
[]
支持比较运算,按字典序
pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
string,字符串
size()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址
queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素
priority_queue, 优先队列,默认是大根堆
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector, greater> q;
stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素
deque, 双端队列
size()
empty()
clear()
front()/back()
push_back()/pop_back()
push_front()/pop_front()
begin()/end()
[]
set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
size()
empty()
clear()
begin()/end()
++, – 返回前驱和后继,时间复杂度 O(logn)
set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,–
bitset, 圧位
bitset<10000> s;
~, &, |, ^
,<<,>>
==, !=
[]
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反
链接:https://www.acwing.com/blog/content/404/
【搜索与图论】
全凑算式 (全排列 + 通分 — 模板)
B DEF
A + --- + ------- = 10
C GHI
(如果显示有问题,可以参见【图1.jpg】)
这个算式中AI代表19的数字,不同的字母代表不同的数字。
比如:
6+8/3+952/714 就是一种解法,
5+3/1+972/486 是另一种解法。
这个算式一共有多少种解法?
注意:你提交应该是个整数,不要填写任何多余的内容或说明性文字。
坑点: 分数通分后相加才为10
A - - - a[0] , B - - - a[1] , C – a[2] , DEF - - - x , GHI - - - y
【通分可整除: (分子1*分母2 + 分子2 * 分母1) % (分母1 * 分母2) == 0 】
#include<algorithm> //swap(a,i,j) next_permutation(a,a+length);
int a[] = {1,2,3,4,5,6,7,8,9};
int ans;
bool check() {
int x = a[3] * 100 + a[4] * 10 + a[5];
int y = a[6] * 100 + a[7] * 10 + a[8]; //通分后可以整除 (才为整数) + 满足等式 == true
if((a[1] * y + a[2] * x) % (y * a[2]) == 0 && a[0] + (a[1] * y + a[2] * x) / (y * a[2] ) == 10 ) {
return true;
}
return false;
}
//递归回溯生成全排列,适用于无重复元素的情况
//考虑第k位,前面的已经排定
void f(int k) { //起始 k = 0 ,f(0)
if(k == 9) { //每种排列 判断
if(check()) {
ans++;
}
}
//全排列模板部分 #include<algorithm>
//从k往后的数字都可以放到k位 i = k
for(int i = k; i < 9; i++) {
//{int t = a[i];a[i] = a[k];a[k] = t;} //swap(a[i],a[k]); 可以替换!
swap(a[i],a[k]);
f(k + 1);//递归
swap(a[i],a[k]);
//{int t = a[i];a[i] = a[k];a[k] = t;} //回溯 swap(a[i],a[k]);
}
}
int test_03() {
//法一:
//已给算法模板全排列
do {
if(check()) {
ans++;
}
} while(next_permutation(a,a+9)); //注意此模板限制条件 !!! 数组a必须是有序的
//f(0); //法二: 手写全排列
cout << ans << endl;
return 0;
}
①全排列【DFS运用 】
#include <iostream>
using namespace std;
const int N = 10;
int n;
int path[N]; //临时存路径值 (每一种排列)
bool st[N]; //标记
void dfs(int u) { //每次填一种,递归 ,u = 0时位于第一层
if(u == n) { //填完n位(u从0开始,path[0]开始)
for(int i = 0; i < n; i++) printf("%d ",path[i]); //输出每条临时路径
puts("");
return;
}
for(int i = 1; i <= n; i++) {
if(!st[i]) { //找到一个没有用过的数
path[u] = i; //填到当前这个位置上去,并且标记i已经用过
st[i] = true;
dfs(u + 1); //递归到最深回来结束时,每一步要恢复现场 【等效每次从不同位置开始遍历,列出所有排列
st[i] = false;//回退
}
}
}
//修改状态,什么时候进入循环,什么时候标记,什么时候出循环,什么时候恢复
int test_01() {
cin >> n;
dfs(0);
return 0;
}
②八皇后【DFS运用 】
顺序 1:每一行开始看,枚举每一行放到哪个位置上去
1)可以列出所有情况, 再判断是否符合条件
2) 可以边填边判断是否合法,不合法就停止,不继续往下递归
【剪枝 - 提前判断】
对角线 y = x + b 、 y = - x + b --> b = y - x || b = y + x [y = n - u当前]
【截距b的值为对角线的编号】 即左上角和右上角一格为对角线/反对角线的第一条
有图…
const int N = 20;
int n;//棋盘大小
int path[N];//存放每轮临时路径【标记用过的】
char g[N][N];//地图
bool col[N] ,dg[N] ,udg[N]; //列标记 , 对角线,反对角线
void dfs(int u)
{
if(u == n)
{
for(int i = 0;i < n;i++) puts(g[i]);//输出i行!!--> 输出整个地图
puts("");
return ;
}
for(int i = 0;i < n;i++) //i行遍历
{
if(!col[i] && !dg[u + i] &&!udg[n - u + i]) //边做边判断
{
path[u] = i;
g[u][i] = 'Q';
col[i] = dg[u + i] = udg[n - u + i] = true;
dfs(u + 1);
col[i] = dg[u + i] = udg[n - u + i] = false; //恢复现场的嵌套对称性!!
g[u][i] = '.';
}
}
}
847.图中点的层次 (BFS求最短距离)
题目描述:
给定一个n个点m条边的有向图,图中可能存在重边和自环。
所有边的长度都是1,点的编号为1~n。
请你求出1号点到n号点的最短距离,如果从1号点无法走到n号点,输出-1。
输入格式
第一行包含两个整数n和m。
接下来m行,每行包含两个整数a和b,表示存在一条从a走到b的长度为1的边。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
数据范围
1≤n,m≤105
输入样例:
4 5
1 2
2 3
3 4
1 3
1 4
输出样例:
1
走k步到达的最短路径【BFS】
#include<algorithm>
#include<cstring>
const int N = 100010;
int n,m;
int h[N],e[N],ne[N],idx;
int d[N],q[N];
void add(int a,int b)
{
e[idx] = b, ne[idx] = h[a] , h[a] = idx++;
}
int bfs()
{
int hh = 0,tt = 0;
q[0] = 1;
memset(d,-1,sizeof d);//初始化 -1不可达
d[1] = 0;//第一个点
while(hh <= tt) //队列不空,每次取队头
{
int t = q[hh++];
for(int i = h[t];i != -1;i = ne[i]) //链表遍历
{
int j = e[i]; //用j表示当前这个点可以到的
if(d[j] == -1)//没有走过
{
d[j] = d[t] + 1; //若j没被扩展d[j]为-1,就j扩展这个点
q[++tt] = j; //入队
}
}
}
return d[n];
}
int test_05()
{
cin >> n >> m;
memset(h,-1,sizeof h);
for(int i = 0;i < m;i++)
{
int a, b;
cin >> a >> b;
add(a,b); //图,此题扩展邻边,即可
}
cout << bfs() << endl;
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≤105
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3
所有的边都是从前指向后的, 入度为0的点可以作为起点(没有任何一条边指向此点,没有前驱)
queue所有入度为0的点可以排在最前面(入队)
while queue不空
{
t = 队头
枚举t的所有出边
t -> j
删掉 t-> j (即d[j]–)
if(d[j] == 0)没有后继了
queue<- j 入队
}
#include<cstring>
const int N = 100010;
int n,m;
int h[N],e[N],ne[N],idx; //邻接表 -- 多条链 -- 多头结点 h[N] = heap[N]
int q[N],d[N]; //存最短
void add(int a,int b) //a-->b 插入边idx的节点编号 a,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;
while(hh <= tt)//队不空
{
int t = q[hh++]; //q[hh++]出队,判断 t节点的路径是否存在环路
for(int i = h[t];i != -1;i = ne[i]) //从t为头开始往下走
{
int j = e[i];//j = i的节点下标
d[j] -- ;
if(d[j] == 0) q[++tt] = j; //到最后一个节点&&没有环路 放入队列中q[++tt]
}
}
return tt == n - 1; //如果最终所有元素入队,说明没有环路!
}
int test_06()
{
cin >> n >> m;
memset(h,-1,sizeof h);
for(int i = 0;i < m;i++)
{
int a,b;
cin >> a >> b;
add(a , b);
d[b] ++ ; //
}
if(topsort())
{
for(int i = 0;i < n;i++) printf("%d ",q[i]); //发现队列中的元素顺序就是topo排序顺序
puts("");
}
else puts("-1");
return 0;
}
851.spfa求最短路 【常用】 【队列 + 数组模拟链表】
记忆点:
int n,m; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 ,多了个权值 w[i]
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; //st标记是否入队
dist[1] = 0; //从1开始
queue q;
q.push(1); //从1开始
st[1] = true;//从1开始
add(a,b,w); spfa();
[类似BFS深搜入队]
while(!q.empty()) { … }
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。数据保证不存在负权回路。
输入格式
第一行包含整数 n 和 m。接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。如果路径不存在,则输出 impossible。
数据范围
1 ≤ n,m ≤ 10 5,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
思路: [类似BFS深搜入队]
把起点放入队列
while(!q.empty())
只要队列里面还有节点变小,就更新
①t = q.front()
q.pop()
② 更新t的所有出边,
找最短min(dist[b] == w) t = b q.push(b)
记忆:邻接表带权值 add(a,b,c),选择c是为了不会数组名w冲突
int n,m; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 ,多了个权值 w[i]
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; // 存储每个点是否在队列中
spfa()第一个节点初始化入队 , dist[1] = 0,q.push(1),st[1] = true【入队标记】
memset(h,-1,sizeof h);memset(dist,0x3f,sizeof dist)
循环中t出队,st[t] = false;
从1开始 -> while(q.size())取队头 标记未加入,链表循环head - >尾部-1结束
j = e[i]判断最短距离 dist[j] > dist[t] + w[t];更新,如果!st[j] ,就加入队列,等于true
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue> //邻接表
const int N = 100010;
int n,m; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 ,多了个权值 w[i]
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
void add(int a,int b,int c) {//选择c是为了不会数组名w冲突
e[idx] = b, w[idx] = c , ne[idx] = h[a] ,h[a] = idx++ ; //a,b编号 ,c权值
}
int spfa() {
memset(dist,0x3f,sizeof dist);
dist[1] = 0; //从1开始
queue<int> q;
q.push(1); //从1开始
st[1] = true;//从1开始
while(q.size()) { //队列不空
int t = q.front();
q.pop();
st[t] = false; //t出队,不在队列中
for(int i = h[t] ; i != -1; i = ne[i]) { //链表遍历 队头t的邻接边,
int j = e[i]; //j代表判断当前边编号e[i] ,判断当前边是否最短路
if(dist[j] > dist[t] + w[i]) { //找最短路
dist[j] = dist[t] + w[i];
if(!st[j]) { //j不在队列中
q.push(j);
st[j] = true;
}
}
}
}
if(dist[n] == 0x3f3f3f3f)return -1;
else return dist[n];
}
int test_04() {
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);//链表初始化-1 ,代表NULL
while(m--) {
int a,b,c;//选择c是为了不会数组名w冲突
scanf("%d%d%d",&a,&b,&c);
add(a,b,c); //邻接表重边没事
}
int t = spfa(); // 可达t = dist[n]; || 不可达 t = -1
if(t == -1) puts("impossible");
else printf("%d\n",t);
return 0;
}
854.Floyd求最短路【多源最短路 - 唯一算法】
题目描述
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出“impossible”。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数n,m,k
接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
接下来k行,每行包含两个整数x,y,表示询问点x到点y的最短距离。
输出格式
共k行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出“impossible”。
数据范围
1≤n≤200 ,
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过10000。
输入样例 (点 | 边 | 查询)
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例
impossible
1
思路:DP 含义,存每个点i到点j的最短距离
邻接矩阵存图 ,存所有的边
d[i,j]
三重循环 i , k , j – > n
#include< algorithm> 中的min要写!
状态转移方程:d[i,j] = min(d[i,j] , d[i,k] + d[k,j] )
#include<algorithm>
#include<cstring>
const int N = 210,INF = 1e9;
int n,m,Q; //n个点 ,m条边 ,Q次询问
int d[N][N];
// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
for(int k = 1;k <= n;k++)
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
d[i][j] = min( d[i][j] ,d[i][k] + d[k][j]);
}
//注释代码,逐块检查
int test_06()
{
scanf("%d%d%d",&n,&m,&Q);
//初始化,自环取0
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n ; j++)
if(i == j) d[i][j] = 0; //自环
else d[i][j] = INF; //初始值
while(m --)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
d[a][b] = min(d[a][b] , w); //重边保留最小边
}
floyd();
while(Q --)
{
int a,b;
scanf("%d%d",&a,&b);
if(d[a][b] > INF / 2) puts("impossible"); //存在负权边,没有被更新不一定是INF
else printf("%d\n",d[a][b]);
}
return 0;
}
最小生成树 【Kruskal】 【贪心】 【结构体数组赋值 + 并查集 + sort最小开始选 + 构造函数赋值 + 并查集合并集合】
- Kruskal算法求最小生成树
给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。
输入格式
第一行包含两个整数n和m。
接下来m行,每行包含三个整数u,v,w,表示点u和点v之间存在一条权值为w的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
数据范围
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
思路:
① 将所有边按权重从小到大排序 O(mlogm) 【贪心】
②枚举每条边a,b 权重c
if a,b不连通 ,将这条边加入集合
【数据结构837.连通块操作 - 并查集的简单应用】 O(m)
记忆点:n个点 == cnt说明有最小生成树 , 添加m条边
并查集find ,edges[N]构造函数赋值 , sort小到大(edges,edges+m)
edges[i] = edges(a,b,w);
如果最后集合中没有全部元素 cnt < n-1则不存在最小生成树
bool operator < (const Edge &W)const { return w < W.w; }
看代码部分,这个是结合题目变型 a = find(a), b = find(b); //找到集合的根【编号】
【遍历m,每一条边】
sort(edges,edges + m); //从最小的权值选,加入
for(int i = 1; i < m; i++) p[i] = i; //并查集初始化
int res = 0,cnt = 0;
for(int i = 0; i < m; i++) { //赋值 ,加入并查集
int a = edges[i].a ,b = edges[i].b , w = edges[i].w;
if(a != b) { //合并集合操作
p[a] = b;
res += w; //最短路径权值和
cnt ++ ; //加入集合(确定最短路)的节点个数
}
}
const int N = 100010;
int n,m;
int p[N];
struct Edge
{
int a,b,w;
Edge(){}
Edge(int _a ,int _b , int _w) {
a = _a;
b = _b;
w = _w;
}
bool operator < (const Edge &W)const { //**记 定义比较 "<"
return w < W.w;//比较w值,且从小到大排序
}
} edges[N];
int find(int x) {
if(p[x] != x ) p[x] = find(p[x]); //递归有函数名
return p[x];
}
int test_02() {
scanf("%d%d",&n,&m);
for(int i = 0; i < n; i++) {
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
//edges[i] = {a,b,c};
edges[i] = Edge(a,b,w);
}
sort(edges,edges + m); //从最小的权值选,加入
for(int i = 1; i < m; i++) p[i] = i; //并查集初始化
int res = 0,cnt = 0;
for(int i = 0; i < m; i++) { //赋值 ,加入并查集
int a = edges[i].a ,b = edges[i].b , w = edges[i].w;
a = find(a), b = find(b); //找到集合的根【编号】
if(a != b) { //合并集合操作
p[a] = b;
res += w;
cnt ++ ;
}
}
if(cnt < n - 1) puts("impossible");
else printf("%d\n",res);
return 0;
}
845.八数码 【最小步数(bfs) 】 【课后习题】
【放】
在一个3×3的网格中,1~8这8个数字和一个“X”恰好不重不漏地分布在这3×3的网格中。
例如:
1 2 3
X 4 6
7 5 8
在游戏过程中,可以把“X”与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让“X”先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
①1 2 3 ② 1 2 3 ③1 2 3 ④ 1 2 3
X 4 6 4 X 6 4 5 6 4 5 6
7 5 8 7 5 8 7 X 8 7 8 X
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入格式
输入占一行,将3×3的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个整数,表示最少交换次数。
如果不存在解决方案,则输出”-1”。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例
19
此题难点:①状态表示复杂【下一步能变成哪些状态】 ②BFS 队列 dist数组记录每个结点的距离【如何运用下标 存3 * 3】
思路: 法一:字符串"9个数" ,queue unordored_map<string,int> dist [字母,位置]
#include<algorithm>
#include<unordored_map>
#include<queue>
int bfs(string start)
{
string end = "12345678x";
queue<string> q;
unordored_map<string , int> d; //到终点的距离
q.push(start);
d[start] = 0;
int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1};
while(q.size())
{
string t = q.front(); // auto t [c++11]
q.pop();
int distance = d[t];
if(t == end) return distance;
//状态转移
int k = t.find('x'); //algorithm 返回下标
int x = k / 3 , y = k % 3; //二维坐标求法
for(int i = 0;i < 4 ;i++)
{
int tx = x + dx[i] , ty = y + dy[i];
if(tx >= 0 && tx < 3 && ty >= 0 && ty < 3 )
{
swap(t[k],t[3 * tx + ty]); // 交换x与下一个状态【二维转一维 坐标】,判断
if(!d.count(t)) //还有没遍历过的 ,
{
d[t] = distance + 1; //到终点的距离,step++ ,达到最终距离后结束
q.push(d);
}
swap(t[k],t[3 * tx + ty]); //恢复状态 ,找最优,所有解
}
}
}
return -1;
}
int test_05()
{
string start;
for(int i = 0;i < 9;i++) //字符串读入
{
char c;
cin >> c;
start += c;
}
cout << bfs(start) << endl;
return 0;
}
【数学知识】
快速幂
typedef long long LL;
int qmi(int x,int k,int p)
{
int res = 1 % p; //p == 1时需要等于0
while(k)
{
if(k % 2) res = (LL)res * x % p;
x = (LL)x * x % p;
k >>= 1;
}
return res;
}
866. 试除法判定质数
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ ) //sqrt(x)
if (x % i == 0)
return false;
return true;
}
867. 分解质因数 [试除法]
有个cnt[ primes[i] --] cout << primes[i] <<" " ;的想法
int cnt; //有想法存放cnt[N]
void divide(int n) //分解结果一定是质数
{
for(int i = 2;i <= n / i;i++)
if(n % i == 0) //枚举到i,说明 n是i的倍数 && 已经除去了2~~i-1质因子,即n,i也不包含任何2 ~~ i-1之间的质因子,[i就一定是个质数],只能被自己和1整除
{
int s = 0;
while(n % i == 0)
{
n /= i;
s++;
}
printf("%d %d\n",i,s); //若要存放:primes[cnt++] = i ,sum[cnt++] = s;
}
if(n > 1) printf("%d %d\n",n,1); //最多可能存在一个 > sqrt(n) 的质因数 [此处优化后,i只要枚举到n/i]
puts("");
}
int main()
{
int n;
cin >> n;
while(n--)
{
int x;
cin >> x;
divide(x);//分解质因数
cout << endl;//注意输出格式
}
return 0;
}
868. 筛质数 【线性筛法】【primes[N]存储质数 + cnt 统计质数的个数】
const int N = 100010;
int primes[N], cnt; // primes[]存储所有素数 ,cnt统计质数个数
bool st[N]; // st[x]存储x是否被筛掉
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 ++ )
{
st[primes[j] * i] = true; //筛去质数primes[j]的倍数
if (i % primes[j] == 0) break; //如果为倍数,已经筛完过 , 结束
}
}
}
869. 试除法求约数
#include<algorithm>
#include<vector>
vector<int> get_divisors(int x)
{
vector<int> res; // 可开全局
for (int i = 1; i <= x / i; i ++ )
if (x % i == 0)
{
res.push_back(i);
if (i != x / i) res.push_back(x / i); // 如果i是x的约数,那么x/i也是x的约数
} //【a|b --> b/a|b 约数成对出现【数论】 , 也是试除法筛质数只要枚举到sqrt(n)的原理】
sort(res.begin(), res.end()); //#include<algorithm>
return res;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
auto res = get_divisors(x); //C++11
for (auto x : res) cout << x << ' '; //C++11
cout << endl;
}
return 0;
}
870.约数个数,871.约数之和
如果 N = p1^c1 * p2^c2 * … *pk^ck 【分解质因数 ,统计质因数的幂】
约数个数: (c1 + 1) * (c2 + 1) * … * (ck + 1) 【即质因数幂+1的乘积】
约数之和: (p10+ p11 + … + p1c1) * … * (pk0 + pk1 + … + pkck)
885. 求组合数 I 【递归法求组合数 - 简写数值小的组合数】 【再求卡特兰数 : Cat(n) = C(2n, n) / (n + 1) 】
c[a][b] 表示从a个苹果中选b个的方案数
for (int i = 0; i < N; i ++ )
for (int j = 0; j <= i; j ++ )
if (!j) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
可以加个typedef long long LL; LL c[N][N];
//用这个 求组合数 和 卡特兰数
const int mod = 1e7 + 10;
const int N = 1000;
int c[1000][1000];
#include<cstdio>
int main()
{
int n;
scanf("%d",&n);
//拿和不拿 C(n,m)
for (int i = 0; i < N; i ++ )
for (int j = 0; j <= i; j ++ )
if (!j) c[i][j] = 1; //第一个C(1,0)
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
for(int i = 1;i <= n;i++)//从1开始
{
printf("%d\n",c[2*i][i]/(i+1) );
}
return 0;
}
【卡特兰数 : Cat(n) = C(2n, n) / (n + 1) 【精确值,放】
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
1. 筛法求出范围内的所有质数
2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + …
3. 用高精度乘法将所有质因子相乘
int primes[N], cnt; // 存储所有质数
int sum[N]; // 存储每个质数的次数
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 ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int get(int n, int p) // 求n!中的次数
{
int res = 0;
while (n)
{
res += n / p;
n /= p;
}
return res;
}
vector<int> mul(vector<int> &A, int b) // 高精度乘低精度模板
{
vector<int> c;
int t = 0;
for (int i = 0; i < A.size() || t; i ++ )
{
if (i < A.size()) t += A[i] * b;
c.push_back(t % 10);
t /= 10;
}
while(c.size() > 1 && c.back() == 0) c.pop_back();
return c;
}
get_primes(a); // 预处理范围内的所有质数
for (int i = 0; i < cnt; i ++ ) // 求每个质因数的次数
{
int p = primes[i];
sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}
vector<int> res;
res.push_back(1);
for (int i = 0; i < cnt; i ++ ) // 用高精度乘法将所有质因子相乘
for (int j = 0; j < sum[i]; j ++ )
res = mul(res, primes[i]);
【【 卡特兰数 : Cat(n) = C(2n, n) / (n + 1)】】 带入试试是否满足样例!
卡特兰数的简单代码实现 【DP】
//函数功能: 计算Catalan的第n项
//函数参数: n为项数
//返回值: 第n个Catalan数
int Catalan(int n)
{
if(n<=1) return 1;
int *h = new int [n+1]; //保存临时结果
h[0] = h[1] = 1; //h(0)和h(1)
for(int i=2;i<=n;++i) //依次计算h(2),h(3)...h(n)
{
h[i] = 0;
for(int j = 0; j < i; j++) //根据递归式计算 h(i)= h(0)*h(i-1)+h(1)*h(i-2) + ... + h(i-1)h(0)
h[i] += (h[j] * h[i-1-j]);
}
int result = h[n]; //保存结果
delete [] h; //注意释放空间
return result;
}
最大公约数-最小公倍数
int gcd(int a,int b)
{
if(b == 0) return a;
return gcd(b,a%b);
}
int lmc(int a,int b)
{
return a / gcd(a,b) * b;//先除再乘 ,防止溢出
}
试除法-分解质因数
//试除法分解质因数 —— 模板题 AcWing 867. 分解质因数
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
int s = 0;
while (x % i == 0) x /= i, s ++ ;
// cout << i << ' ' << s << endl;
}
// if (x > 1) cout << x << ' ' << 1 << endl;
cout << endl;
}
组合数dp法
// c[a][b] 表示从a个苹果中选b个的方案数
for (int i = 0; i < N; i ++ )
for (int j = 0; j <= i; j ++ )
if (!j) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod; //逆推,取第b个时是最后一个还是前面的(最后一个取/不取)
分解质因数法求组合数 —— 模板题 AcWing 888. 求组合数 IV
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
1. 筛法求出范围内的所有质数
2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + …
3. 用高精度乘法将所有质因子相乘
【由数据范围反推算法复杂度以及算法内容】
链接:https://www.acwing.com/blog/content/32/
一般ACM或者笔试题的时间限制是1秒或2秒。
在这种情况下,C++代码中的操作次数控制在 107~108为最佳。
n≤30, 指数级别, dfs+剪枝,状态压缩dp
n≤100 => O(n3),floyd,dp,高斯消元
n≤1000 => O(n2),O(n2logn),dp,二分,朴素版Dijkstra、朴素版Prim、Bellman-Ford
n≤10000 => O(n3/2),块状链表、分块、莫队
n≤100000 => O(nlogn) => 各种sort,线段树、树状数组、set/map、heap、拓扑排序、dijkstra+heap、prim+heap、Kruskal、spfa、求凸包、求半平面交、二分、CDQ分治、整体二分、后缀数组、树链剖分、动态树
n≤1000000 => O(n), 以及常数较小的 O(nlogn)O(nlogn) 算法 => 单调队列、 hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的 O(nlogn)O(nlogn) 的做法:sort、树状数组、heap、dijkstra、spfa
n≤10000000 => O(n),双指针扫描、kmp、AC自动机、线性筛素数
n≤109 => O(n1/2),判断质数
n≤1018 => O(logn),最大公约数,快速幂,数位DP
n≤101000 => O((logn)2),高精度加减乘除
n≤10100000 => O(logk×loglogk),k表示位数O(logk×loglogk),k表示位数,高精度加减、FFT/NTT
【动态规划】
01背包问题
#include <iostream>
using namespace std;
#include<algorithm>
const int N = 1010;
int v[N],w[N];
int dp[N];
int f[N][N]; //爆栈直接导致无法编译 。。。各种问题
int n,m;
int test_01()
{
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]; //左边状态
if(j >= v[i]) f[i][j] = max(f[i][j] , f[i-1][j - v[i]] + w[i]); //右边状态
}
cout << f[n][m] << endl;
return 0;
}
//转化成一维:可行性条件 - 滚动数组 不管哪一种均需满足 总体积小于j ,且式子是包含关系,可以合并
int test_02()
{
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 = m;j >= v[i];j--) //若正的取变成 f[i][j - v[i]] + w[i] 而不是i-1
dp[j] = max(dp[j] , dp[j - v[i]] +w[i]);
cout << dp[m] << endl;
return 0;
}
完全背包问题 【与01背包不同的是j为正序 - 由推导过程得出】
#include<algorithm>
const int N = 1010;
int v[N],w[N];
int dp[N];
int f[N][N]; //爆栈直接导致无法编译 。。。各种问题
int n,m;
int test_03()
{
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 = 0;j <= m;j++)
for(int k = 0;k * v[i] <= j;k++) //最坏v[i] = 1 ,循环j次 O(10^9^)
{
f[i][j] = max(f[i][j],f[i-1][j - k * v[i] ] + k * w[i]);
}
cout << f[n][m] << endl;
return 0;
}
int test_04()
{
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 = v[i];j <= m;j ++)
dp[j] = max(dp[j] , dp[j - v[i] ] + w[i]);
cout << dp[m] <<endl;
return 0;
}
多重背包问题
const int N = 1010;
int v[N],w[N],s[N];
int dp[N];
int f[N][N]; //爆栈直接导致无法编译 。。。各种问题
int n,m;
#include<algorithm>
int test_05()
{
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 = 0;j <= m;j++)
for(int k = 0; k <= s[i] && k * s[i] <= j;k++) //k <= s[i],k数量最多s[i] && 能放得下
f[i][j] = max(f[i][j] , f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m] << endl;
return 0;
}
898.数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数n,表示数字三角形的层数。
接下来n行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1 ≤ n ≤ 500,
-10000 ≤ 三角形中的整数 ≤ 10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
动态规划:
状态表示
集合:所有从起点,走到(i,j)的路径
属性:Max
状态计算(分类,从左边走下来/右边走下来的)
f[i][j] = max( f[i-1][j-1] + a[i][j] ,f[i-1][j] + a[i][j]); //加上当前点的坐标,比较取最大值
#include <iostream>
using namespace std;
#include<algorithm>
const int N = 510,INF = 1e9;
int n;
int a[N][N];
int f[N][N];
int test_01()
{
scanf("%d",&n);
for(int i = 1;i<= n;i++)
for(int j = 1;j <= i;j++)
scanf("%d",&a[i][j]);//dp数组从1开始,记录三角形走到每个位置的最大值
for(int i = 0;i<= n;i++)
for(int j = 0;j <= i + 1;j++) //数字三角形初始化 i+1是因为dp比较时会越界,越出三角形,所以把哪些位置赋值负无穷
f[i][j] = -INF;
f[1][1] = a[1][1];//起点位置
for(int i = 2;i <= n;i++)//i从第2行开始
for(int j = 1; j <= i;j++)
f[i][j] = max(f[i-1][j-1] + a[i][j] , f[i-1][j] + a[i][j]);
int res = -INF;
for(int i = 1;i <= n;i++) res = max(res , f[n][i]);//最终答案要遍历一下最后一行,取走到最后一行的最大值中比较 再取最大值
cout << res << endl;
return 0;
}
895. 最长上升子序列
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000,
-109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
动态规划
状态表示
集合:所有第i个数结尾的上升子序列
属性:Max - - 以i结尾长度最大值
状态计算
把数排列【a[i]>a[j],那么a[i]的最长序列长度为f[j]+1 (比j多1) 】
f[i] = max(f[i] , f[j] + 1) j = 0 ,1 , 2 … i - 1
const int N = 1010;
int n;
int a[N],f[N];
int test_02()
{
scanf("%d",&n);
for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
for(int i = 1;i <= n;i++)
{
f[i] = 1;
for(int j = 1;j <= i;j++)
{
if(a[j] < a[i]) //满足条件,更新j的长度,比较长度
f[i] = max(f[i] , f[j] + 1);
}
}
int res = 0;
for(int i = 1;i <= n;i++) res = max(res , f[i]); //取最大长度
cout << res << endl;
return 0;
}
石子合并 【区间DP 类似合并果子】
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;
如果第二步是先合并 2,3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N 表示石子的堆数 N。
第二行 N 个数,表示每堆石子的质量(均不超过 1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22
动态规划
状态表示 f[i][j]
集合:所有将第i堆石子到第j堆石子的合并方式
属性 Min
状态计算
所有石子最后一步都是两边的合并【[i,j]区间 i左j右 最后为 f[i][k] 与 f[k+1][j]合并】,【哪些状态转移到最后一步】
每一类的最小代价取Min
f[i][j] = min(f[i][k] + f[k +1][j] +s[j] - s[i - 1]) (k = i - j + 1)
O(n3) N = 300时 ~= 270万 ~ 2.7秒
const int N = 310;
int n;
int s[N]; //前缀和数组 , 代价值用利用前缀和数组求区间和 计算
int f[N][N];
int test_05()
{
scanf("%d",&n);
for(int i = 1;i <= n;i++)scanf("%d",&s[i]);
for(int i = 1;i <= n;i++) s[i] += s[i - 1]; //初始化 s[i] = s[i - 1] + a[i] (这里因为a[i]已经存入s[i]中,所以这样写等效)
for(int len = 2;len <= n;len ++) //len = 1代价是0,从len = 2开始即可 ,区间长度
for(int i = 1;i + len - 1 <= n;i ++) //区间的左端点
{
int l = i,r = i + len - 1;
f[l][r] = 1e8;//初始化,否则每次都是0
for(int k = l;k < r;k ++) //DP计算
f[l][r] = min(f[l][r] , f[l][k] + f[k + 1][r] + s[r] - s[l - 1]); // 搬动[l,r] 的代价为 s[r] - s[l - 1]
}
cout << f[1][n] << endl;//将第1堆到第n堆合并为1堆的最小代价 !!!为f[1][n]
return 0;
}
900. 整数划分 - 计数类DP
1、题目描述
一个正整数n可以表示成若干个正整数之和,形如:n=n1+n2+…+nk 其中n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数n的一种划分。
现在给定一个正整数n,请你求出n共有多少种不同的划分方法。
输入格式
共一行,包含一个整数n。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对109+7109+7取模。
数据范围
1≤n≤1000
输入样例:
5
输出样例:
7
动态规划
状态表示
集合:所有总和是i , 并且恰好表示成j个数的和的方案
属性:数量
状态计算
分两类:每个数比较最小值是1 | 最小值大于1
方案 f[i - 1][j - 1]代表和为 i - 1,有j-1个数 的方案数 , f[i - j][j] 和是i - j ,有j个数 的方案
f[i][j] = f[i - 1][j - 1] + f[i - j][j];
ans = f[n][1] + f[n][2] + … + f[n][n];
const int N = 1010,mod = 1e9 + 7;
int n;
int f[N][N];
int test_06()
{
cin >> n;
f[0][0] = 1;
for(int i = 1;i <= n;i ++)
for(int j = 1;j <= i;j ++)
f[i][j] = f[i - 1][j - 1] + f[i - j][j];
int res = 0;
for(int i = 1;i <= n;i++) res += f[n][i];
cout << res << endl;
return 0;
}
【贪心】【先选最优 – sort】
148. 合并果子 【优先队列-push时自动排序(默认大根堆[大到小])】
在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。
达达决定把所有的果子合成一堆。
每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。
可以看出,所有的果子经过 n?1 次合并之后,就只剩下一堆了。
达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。
假定每个果子重量都为 1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。
例如有 3 种果子,数目依次为 1,2,9。
可以先将 1、2 堆合并,新堆数目为 3,耗费体力为 3。
接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12,耗费体力为 12。
所以达达总共耗费体力=3+12=15。
可以证明 15 为最小的体力耗费值。
输入格式
输入包括两行,第一行是一个整数 n,表示果子的种类数。
第二行包含 n 个整数,用空格分隔,第 i 个整数 ai 是第 i 种果子的数目。
输出格式
输出包括一行,这一行只包含一个整数,也就是最小的体力耗费值。
输入数据保证这个值小于 231。
数据范围
1≤n ≤10000,
1≤ai ≤20000
输入样例:
3
1 2 9
输出样例:
15
思路:
每次选取最小的两堆合并成一堆,最终合并成一堆。
优先队列实现
priority_queue<int, vector< int >, greater< int > > heap;//小根堆【小到大】
priority_queue< int >//默认是大根堆
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
//优先队列加参数 变成 小根堆 逆序从大到小 定义参数为固定要写的,要用就要背
priority_queue<int, vector<int>, greater<int> > heap;
while (n -- )
{
int x;
scanf("%d", &x);
heap.push(x);
}
int res = 0;
while (heap.size() != 1)
{
int a = heap.top(); heap.pop();
int b = heap.top(); heap.pop();
res += a + b;
heap.push(a + b); // 注意这里是push a+b,不是push res
}
printf("%d\n", res);
return 0;
}
913.排队打水 【sort】
有 n 个人排队到 1 个水龙头处打水,第 i 个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?
输入格式
第一行包含整数 n。
第二行包含 n个整数,其中第 i 个整数表示第 i 个人装满水桶所花费的时间 ti。
输出格式
输出一个整数,表示最小的等待时间之和。
数据范围
1≤n ≤105
1≤ti ≤104
输入样例:
7
3 6 1 4 2 5 7
输出样例:
56
思路 :假设一种做法,证明这种做法的正确性,如反证法,调整法
每个人等没有人打水 才可以用,轮流打水 ,【让打水时间最多的人最后打水,它就不用让别人等他打水的时间 】
最优解: sort(a,a+n) , a[i] * (n - i - 1 ),后面的每个人都要等
最前面后面的人一起等待a[i]时间,每次打完水,等待的人减一
#include<algorithm>
const int N = 100010;
typedef long long LL;
int n;
int t[N];
int test_01()
{
scanf("%d",&n);
for(int i = 0;i < n;i++) scanf("%d",&t[i] );
sort(t,t+n);
LL res = 0;
for(int i = 0;i < n;i++) res += t[i]*(n - i - 1);
cout << res << endl; //prnitf("%lld",res);
return 0;
}
104. 货仓选址
在一条数轴上有 N 家商店,它们的坐标分别为 A1~AN。
现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。
为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
输入格式
第一行输入整数 N。
第二行 N 个整数 A1~AN。
输出格式
输出一个整数,表示距离之和的最小值。
数据范围
1≤N ≤100000,
0≤Ai ≤40000
输入样例:
4
6 2 9 1
输出样例:
12
思路:
求f(x) = |x[1] - x| + |x[2] - x| + |x[3] - x| + |x[4] - x| + |x[5] - x| + |x[6] - x| + … +|x[n] - x|
只看其中两项
绝对值不等式:
|x - a| + | x - b | >= |a - b| x在[a,b]区间内 , 等号成立
即等号成立时最小,可以发现x处于每一项的中位数,就都可以取最小值
思路:
sort( a , a + n); //数轴排序
res += abs(a[i] - a[n / 2]);
#include<iostream>
using namespace std;
#include<algorithm>
const int N = 100010;
int n;
int a[N];
int test_02()
{
scanf("%d",&n);
for(int i = 0;i < n;i ++ ) scanf("%d",&a[i]);
sort( a , a + n); //数轴排序
int res = 0;
for(int i = 0;i < n ;i ++) res += abs(a[i] - a[n / 2]); //最短距离为 与中位数距离
cout << res << endl;
return 0;
}