文章目录
- 基础算法
- 数据结构
- 数学知识
- 866. 试除法判定质数
- 868. 筛质数 ( (朴素)埃氏筛法、 线性筛法)
- 分解质因数
- 869. 试除法求约数 (试除法)
- 870.约数个数
- 871. 约数之和
- 872. 最大公约数
- 欧拉函数
- 877. 扩展欧几里得算法
- 高斯消元 - 解方程 [暂放 不能写出0/1]
- 885. 求组合数 I C(m,n) 【dp】
- 886 求组合数 II 【数据大小10万级别】 【费马小定理+快速幂+逆元】
- 887. 求组合数 III 【le18级别】 【卢卡斯定理 + 逆元 + 快速幂 】
- 888.求组合数 IV 【没有%p -- 高精度算出准确结果】 【分解质因数 + 高精度乘法 --只用一次高精度提高运行效率】
- 889.满足条件的01序列 【卡特兰数-用法极多!】
- 890.能被整除的数(容斥原理)
- 891.Nim游戏
- 动态规划
前言:1秒 执行循环约 107 ~ 108 (一亿以内)
AcWing简易版速看模板
ACM听课杂糅
赛前准备的题型
罗勇军考点直播
基础算法
排序,查找 , 高精度,差分 , 双指针
(高精度 较为少用 ,实际仅仅排序调用sort()即可 )
自认为低频模板: 堆 。
快速排序
注意: 选择初始标记有两种要点(除了左右边界可能出问题,其他都可以选)
一种是:用L ~j和 j + 1 ~ R,这时不能选择右边界[统一用x = q[l] 选择左边界]
另一种是用 L ~ i - 1 和 i ~ R; 这时不能选择左边界[统一用x = q[r] 选择右边界] (即不能取到L,会死循环)
记:①先判断是否L<R【也是递归出口】【取x=q[L],起点左->右找到第一个大于x的数(下标),末尾右->左找到第一个小于x的数,用(下标)交换两个数位置】 ② 起点需在边界两侧在i = L-1和j = R+1扫描一遍数组while(i < j),则先do下标i++,再循环判断寻找左边第一个大于x和右边往左第一个小于x的位置 ,若i<j 为了满足从大到小 , 需交换 , 最后再分成 l–>j 和 j+1–> r 递归 ,x取q[l+r>>1]划分比较合理
#include <iostream>
using namespace std;
//#include<algorithm> 现成快排 sort(q,q+n);
const int N = 1e6 + 10;
int n;
int q[N];
void quick_sort(int q[],int l,int r) { //l,r 边界下标
if(l >= r) return ; //判断,只有一个数不用排序 【同时也是递归出口,缺少会死循环bug】
//
int x = q[l+r>>1],i = l - 1,j = r + 1; // 边界指针赋值从 -1 , r+1 防止边界先比较一次的判断出错 从边界两侧
while(i < j) {
do i++ ;while(q[i] < x); //从左边开始++ 判断,找到第一个大于标记x的
do j-- ;while(q[j] > x); //【改成q[j]<x就变成从大到小!】从右边开始-- 判断,找到第一个小于标记x的
if(i < j)swap(q[i],q[j]); //检查,大的放右边,i已经超过就说明满足不用调换位置
}
quick_sort(q,l,j),quick_sort(q,j + 1,r); //l左边界初始0,j右指针, r右边界初始len - 1, 用j分成两部分递归遍历 ! (当然也可以用i ,但上面也要改)
}
/*
10
3 5 7 8 1 4 6 9 2 10
*/
int main() {
scanf("%d",&n);
for(int i = 0; i < n; i++)scanf("%d",&q[i]);
quick_sort(q,0,n - 1);
for(int i = 0; i < n; i++)printf("%d ",q[i]);
return 0;
}
归并排序
【原理】:分每组第一层从l和mid+1开始两个比较,不断归并组,直到每个组剩一个元素(即一个元素一组),排序完毕 。 (分左右两边递归排好序,递归过程越分越小,每组一个后返回,合并)
复杂度:n不断除2 ,每次(层)扫描n个, 最终O(nlogn)
具有稳定型(相同值的数的相对位置不会改变)
记:(也要先判断边界下标L<=R)【先递归分组,中间位置指针 mid = l+r >> 1 , 分开扫描l–>mid,mid+1–>r [i<=mid,j<=r],比较交换 , 最后两重循环把其中一个数组还剩的一些较大元素放入 , k=0计数放入最终排序的第几个, 排序到tmp临时数组 , 最后再放回q数组的对应区间中i=l -->r ,i<=r,j=0 ,i++,j++ 】
输入:
10
3 5 7 8 1 4 6 9 2 10
输出:
1 2 3 4 5 6 7 8 9 10
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N],tmp[N]; //N = 1e6 + 10; 归并要 tmp辅助数组 ,最后赋值回q数组完成排序,当然(tmp开全局,也可以tmp当做答案)
void merge_sort(int q[],int l,int r) {
if(l >= r) { //当前区间仅有1个数或者没有数 ,不用再排序了 ,结束
return;
}
int mid = l + r >> 1; // >>1 位运算即 /2
//先分成两部分每部分排好序[递归 计算比较第一步 从最小的两个比较开始]
merge_sort(q,l,mid),merge_sort(q,mid+1,r);//递归排序 *此模板先写*
int k = 0,i = l,j = mid + 1; // l --- mid + 1
while(i <= mid && j <= r) { //边界限制
if(q[i] <= q[j] ) tmp [k++ ] = q[i++ ];//从小到大排序
else tmp [k++] = q[j++ ];
while(i <= mid) tmp[k++] = q[i++]; //若循环完,如一边全部小于第一个,其他全部接在后面
while(j <= r) tmp[k++] = q[j++];
for(i = l,j = 0 ; i <= r; i++ , j++) q[i] = tmp[j]; //把tmp[]赋值给q数组的对应区间 ,实现q数组的排序
}
int main() {
scanf("%d",&n);
for(int i = 0; i < n; i++)scanf("%d",&q[i]);
merge_sort(q,0,n - 1);
for(int i = 0; i < n; i++)printf("%d ",q[i]);
return 0;
}
整数二分模板
有单调性一定可以二分,没有单调性也有可能二分(不是本质)
若能找到某种性质,如右半边满足,左半边不满足 ,二分就可以寻找边界
check是满足题目的某种条件 【比如 q[mid] >= x】
判断true时答案在哪个区间里面 判断是 l = mid ,还是 r = mid (选择答案所在的区间进行处理)
一定有解,若题目无解,通过check检查出 ,如q[L] != x ,无解
while(l<r){ 模板 } return L;
【模板1】 if(check(mid)==true) l —> R = mid, else L = mid+1 —> r 分两半,且L <= mid,R >= mid
【模板2】mid = l+r + 1 >> 1 要加1(否则死循环), if(check(mid)) L = mid;else R = mid - 1; 返回L
模板选择: 判断 mid 在划分后答案的范围 【写完不对就换另一种呗 ,无脑穷举】
记: 先写mid = l + r >> 1; 看答案划分后在[mid+1,r]还是[mid,r] , 确定 r = mid 还是 r = mid + 1
mid = l + r >> 1;对应r=mid,l=mid+1 [l,mid]和[mid+1,r] ; mid=l+r+1>>1对应r=mid+1,l=mid,[l,mid-1][mid,r]
bool check(int x) //检查x是否满足某种性质
//区间[l , r] 被划分成 [l,mid] 和 [mid + 1,r]时使用(r=mid,或l=mid+1递归)
bool check(int mid) {
return true;
}
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; //l = mid + 1 配对 r = mid 分两半
}
return l; //若l=0,说明check(mid) == false 没有进行二分,无解
}
//区间[l , r] 被划分成 [l,mid - 1] 和 [mid,r]时使用
int bsearch_2(int l,int r) {
while(l < r) {
int mid = l + r + 1 >> 1; //不补+1 , 当l = r - 1时 ,check成功 mid = l且一直循环 = l ,无限死循环
if(check(mid)) l = mid;
else r = mid - 1; //l = mid 配对 r = mid - 1 分两半
}
return l; //统一l ,当然r也行(最终结束位置 l == r)
}
例题:数的范围 (整数二分)
题目
给定一个按照升序排列的长度为n的整数数组,以及 q 个查询。(已经排好序)
对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)。
如果数组中不存在该元素,则返回“-1 -1”。(比如有多个相同的数,一个为头部位置,一个为尾部位置)
输入格式
第一行包含整数n和q,表示数组长度和询问个数。
第二行包含n个整数(均在1~10000范围内),表示完整数组。
接下来q行,每行包含一个整数k,表示一个询问元素。
输出格式
共q行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回“-1 -1”。
数据范围
1 ≤ n ≤ 100000 1 ≤ q ≤ 10000 1 ≤ k ≤ 10000
1≤q≤10000
1≤k≤10000
输入样例
6 3
1 2 2 3 3 4
3
4
5
输出样例 (边输入,变输出 可AC)
3 4
5 5
-1 -1
int n,m;
int q[N];
//例题1:check性质定义成 q[mid] <=x (左边 <= 第一个x) ,即最终找到第一个x 第二次q[mid] >=x ,找到倒数第一个x
#include<bits/stdc++.h>
using namespace std;
int main() {
scanf("%d%d",&n,&m);
for(int i = 0; i < n ; i++) scanf("%d",&q[i]);
while(m--) { //循环二分查找
int x;
scanf("%d",&x);
int l = 0,r = n - 1;
while(l < r) { //左边界 < 右边界
int mid = l + r >> 1; //先写上 再考虑要不要加1
if( q[mid] >= x) r = mid ; //比较
else l = mid + 1; //否则在右边
}
if(q[l] != x) cout << "-1 -1" << endl; //查找结束指针位置为l 比较值,判断是否找到
else { //找到
cout << l << " ";
int l = 0,r = n - 1; //重新搜索, 边界赋回初始值
while(l < r) {
int mid = l + r + 1 >> 1; // mid可能是答案,所以 l = mid ,配对 r = mid - 1 --对应第二个模板 mid = l + r + 1 >> 1;
if(q[mid] <= x) l = mid; //找第一个x
else r = mid - 1;
}
cout << l << endl;
}
}
return 0;
}
浮点数二分: 查找x的开方数值 x1/2
x1/2 查找范围[0,x]
【经验值:多两位精度,如保留6位小数,用r-l = 1e-8】
浮点数二分 :check后递归都是 R 或 L = mid ,不用判断边界问题
#include<bits/stdc++.h>
using namespace std;
int mian(){
double x;
cin >> x;
double l = 0,r = x; //精确小数位数 1e-6后6位
//for(int i = 0;i < 100;i++)//法二:直接循环100次 等效把整个区间的长度 除以 2^100^ ~ 1e-6的精度
while(r - l > 1e-8) //两个数的差 精度差别很小 -近似认为一个数 可以取l或者r作为ans ,
{
double mid = (l + r) / 2;
if(mid * mid >= x) r = mid; //*浮点数二分 不用加一或者减一* --> r = mid 配对 l = mid
else l = mid;
}
printf("%lf\n",l);
return 0;
}
前言:苦命的C++选手没有现成的高精度 ~~emo
C++高精度模板
由A <= 106 变成len(A) <= 106 (即可以存储106位数!!!)
数组下标 : 0 1 2 3 4 5 6 7 8
对应存放: 9 8 7 6 5 4 3 2 1
原因:若在数组中进位,补高位需要全部往右平移一格,而若在低位补就很方便
注意:因此主函数中逆序存放A,B,数组C的遍历从末尾开始
for(i = C.size() - 1 ; i >= 0;i-- )printf(“%d”,C[i]);
大数存储: 来自低位的进位 | 对更高位的进位(加一个位数,且若有进位肯定只能加1)
A3 A2 A1 A0
+ B2 B1 B0
_______________
------ C1 C0
新数组里面每一位存储的就是 A[i] + B[i] + t(低位的进位 0或1 )
791.高精度加法
题目描述:
给定两个正整数,计算它们的和。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的和。
数据范围
1≤整数长度≤100000
输入:
1111111111111111111111111
22222222222222
输出:
1111111111133333333333333
链接:https://www.acwing.com/blog/content/277/
更简洁一点的写法,先把大的放前面 add(大,小)
用大.size()遍历 , if(i < 小.size()) t += 小[i] ,t是每一位的数
注意点:读入是string a,b;逆序读入数组中
for(int i = a.size() - 1;i >= 0;i–) A.push_back(a[i] - ‘0’); 【记】
if(t) C.push_back(1); //如果两数相等,且有进位,t != 0,进1
C也是逆序输出:for(int i = C.size() - 1;i >= 0;i–) printf(“%d”,C[i]);
写法1:判大小:可调换保持 add(大,小)
// C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return add(B, A);//先把大的放前面 add(大,小)
vector<int> C;
int t = 0;
for (int i = 0; i < A.size(); i ++ ) //用大.size()遍历
{
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;
}
推荐写法二:
简记: vector<int> add(vector<int> &A ,vector<int> &B ) 参数为 引用
for(int i = 0;i < A.size() || i < B.size();i++)
vector<int> A.或B.size() ,(size内位数值非0),
t加A,B每位上的值 ,存C.push_back(t%10) ; t /= 10;
遍历完后,最高位看是否进位: if(t)C.push_back(1); return C;
main: string a,b逆序读入到vector<int> A,B中 ,读入A.push_back(a[i] - ‘0’) string -->int数值:ASCLL码差值
C逆序输出 :for(int i = C.size() - 1;i >= 0;i--) printf("%d",C[i]);
#include <iostream>
#include<cstdio>
using namespace std;
#include<vector>
//C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> &A ,vector<int> &B )
{
vector<int> C;
int t = 0; //来自低位的进位
for(int i = 0;i < A.size() || i < B.size();i++){ //A,B已经逆序存放,所以正序判断即从个位开始判断
if(i < A.size()) t += A[i];//判断每一位 有数就加上
if(i < B.size()) t += B[i];
C.push_back(t % 10); //t 赋值为 A[i] + B[i] + t , t % 10 存余数
t /= 10; //看t要不要进入下一位 (更高位) 大于10,t变为1 --> true
}
if(t) C.push_back(1); //进位到 更高位 赋1
return C;
}
int main(){
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;
}
792.高精度减法 (模板定为A,B为正整数 若A-B < 0 前面先输出"-",再逆序输出C)
C = A - B
①若A < B ,交换AB 算B - A ,前面加一个负号 ‘-’
②若A > B ,
t = A[i] - B[i] - t (t表示当前这一位的值)
若 t < 0 返回t+10 ,若t > 0 返回t -->合二为一 : (t + 10) % 10
给定两个正整数,计算它们的差,计算结果可能为负数。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的差。
数据范围
1 ≤ 整 数 长 度 ≤ 105
输入样例1:
11111111111111111111111111
11111111111102
输出样例1:
11111111111100000000000009
//判断是否有 A >= B
bool cmp(vector<int> &A,vector<int> &B) { //cmp【比较】
if(A.size() != B.size()) return A.size() > B.size(); //先比较位数
for(int i = A.size() - 1; i >= 0 ; i--) {
if(A[i] != B[i]) { //从高位开始比
return A[i] > B[i];
}
}
return true; //都相等,则A == B
}
vector<int> sub(vector<int> &A ,vector<int> &B ) { //t表示当前这一位的值
vector<int> C;
for(int i = 0,t = 0; i < A.size(); i++) { //初始t表示是否借位 0/1 ,一就表示低位(不够减了)向高位借位
t = A[i] - t;
if(i < B.size()) t -= B[i]; //i代表A的位数,若剩下的高位就不用减B,因为B未赋值,剩下的直接放入C中就可以了
C.push_back((t + 10) % 10);
if(t < 0) t = 1;//判断要不要借位,小于0,向前面借一位 (即判断下一位要减一,先把t初始为1 ,减去t时等效减一)
else t = 0;
}
//去掉前导0
while(C.size() > 1 && C.back() == 0)C.pop_back();
//如 减完结果 003 ,前面的0要舍去 | C.back()就表示最高位若为0舍去 |但如果这个数是0还是要保留
return C;
}
#include<string>
int main() {
string a,b; //举例 :a = "123456";
vector<int> A,B,C;//my~先创建好ABC
cin >> a >> b;
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'); //字符串存的字符型整数 要转成真正的整数 int型
if(cmp(A,B)) { //注意:减法要先判断是否 A >= B ,若A < B 则要交换一下,且最后加个负号
C = sub(A,B);
for(int i = C.size() - 1; i >= 0; i--) printf("%d",C[i]);
} else { //相减结果小于0 , 先加个负号
C = sub(A,B);
printf("-");
for(int i = C.size() - 1; i >= 0; i--) printf("%d",C[i]);
}
return 0;
}
793.高精度乘法
题目描述:
给定两个正整数A和B,请你计算A * B的值。
输入格式
共两行,第一行包含整数A,第二行包含整数B。
输出格式
共一行,包含A * B的值。
数据范围
1≤A的长度≤100000,
1≤B≤10000
输入样例:
11111111111111111111111111111111111111111111111
9
输出样例:
99999999999999999999999999999999999999999999999
思路:
把(int)B看做一个整体和(string)A每位相乘
#include<string>
#include<iostream>
#include<vector>
using namespace std;
vector<int> mul(vector<int> &A,int b) {
vector<int> C;
int t = 0;//进位
for(int i = 0; i < A.size() || t; i++) { //【清除前导0】 C的最后一位是 乘积的第一位
if(i < A.size()) t += A[i] * b;
C.push_back(t % 10);//只取t的个位
t /= 10;//整除10后是进位
}
while (C.size() > 1 && C.back() == 0) C.pop_back();//【清除前导0】 C的最后一位是 乘积的第一位
return C;
}
int main() {
string a;
int b;//注意b为一个整体!!
cin >> a >> b;
vector<int> A,C;
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0'); //a[i] 里面存的是字符型整数,要转化为真正的整数 需减去 '0' 成int型
C = mul(A,b);
for(int i = C.size() - 1; i >= 0; i--) printf("%d",C[i]);
return 0;
}
794. 高精度除法 (高精度A / 低精度B)
题目:
给定两个非负整数A,B,请你计算 A / B的商和余数。
输入格式
共两行,第一行包含整数A,第二行包含整数B。
输出格式
共两行,第一行输出所求的商,第二行输出所求余数。
数据范围
1≤A的长度≤100000,
1≤B≤10000
B 一定不为0
输入样例:
1000000000000000000000000000000000000000000000000000000
9
输出样例:(商 、余数)
111111111111111111111111111111111111111111111111111111
1
思路: 结果的每一位为余数
(余数r * 10 + 被除数的下一位A[i] ) / 除数 == 新的余数r/b存入C ,r更新为新的余数 r %= b
#include<algorithm>
vector<int> div(vector<int> &A,int b,int &r) { //参数多了 &r //正着会更好算,但是为了高精度运算统一,都逆序存
vector<int> C;
r = 0;//初始余数0
for(int i = A.size() - 1; i >= 0; i--) { //先正着算出结果
r = r * 10 + A[i]; //余数
C.push_back(r / b); //除以除数得到的下一位余数存入C
r %= b; //变成下一位余数
}
reverse(C.begin(),C.end()); //再逆序存放 #include<algorithm>
//除去前导0
while(C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
int main() {
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'); //a[i] 里面存的是字符型整数,要转化为真正的整数 需减去 '0' 成int型
int r;
C = div(A,b,r);
for(int i = C.size() - 1; i >= 0; i--) printf("%d",C[i]);
cout << endl << r << endl; //输出余数
return 0;
}
795.前缀和差分
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式
共 m 行,每行输出一个询问的结果。
数据范围
1≤l≤r≤n,
1≤n,m≤100000,
−1000≤数列中元素的值≤1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10
前缀和数组下标从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判断)
const int N = 100010; //如果修改数据只要在一个地方修改,而且0很多时就不会写错
int n,m;
int a[N],s[N];
int main(){
//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--)
{
int l,r;
scanf("%d%d",&l,&r);
printf("%d\n",s[r] - s[l - 1]); //区间和的计算
}
return 0;
}
空间优化法,但不保存原数组 a[N]
for(int i = 1;i <= n;i++) scanf("%d",&s[i]); //减少空间优化法,但不保存原数组
for(int i = 1;i <= n;i++) s[i] += s[i - 1]; //前缀和的初始化
796.子矩阵的和 【二维前缀和】
输入一个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]
【画图想象子矩阵的加 == 用大矩阵全加上再减去部分不属于子矩阵的】
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m,q; //q次询问
int a[N][N],s[N][N];
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i = 1;i <= n;i++){ //从1开始
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++){ //(0,0) ~ (i,j) 的子矩阵和 (容斥原理,有些类似DP)
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]); //求(x1,y1) ~ (x2,y2)子矩阵的和
}
return 0;
}
797. 差分
输入一个长度为n的整数序列。
接下来输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数n和m。
第二行包含n个整数,表示整数序列。
接下来m行,每行包含三个整数l,r,c,表示一个操作。
输出格式
共一行,包含n个整数,表示最终序列。
数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2
思路分析.记:
给定a[1],a[2],…a[n]构造差分数组b[n],使得
a[i]=b[1]+b[2]+…+b[i]【初始化仍用差分赋值a[i],同时也操作赋值了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,利用差分函数构造差分数组。
使得前缀和数组a的区间增减仅需O(1)复杂度
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,最后]不受影响
}
#include <iostream>
using namespace std;
int N = 1e5 + 5;
int a[N],b[N];
int n,m;
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的区间不受影响】
}
int main()
{
int l,r,t;
cin>>n>>m;
for(int i=1;i<=n;i++){ //下标从1开始,差分赋值初始化a[i]和b[i]
cin>>a[i];
insert(i,i,a[i]); //初始化b[i]
}
while(m--){//m次差分操作【题目要求】
cin>>l>>r>>t;
insert(l,r,t);
}
t=0;
for(int i=1;i<=n;i++){
t+=b[i];//对差分数组求和得到区间增减后的数组
cout<<t<<" ";
}
return 0;
}
798.差分矩阵
输入一个n行m列的整数矩阵,再输入q个操作,每个操作包含五个整数x1, y1, x2, y2, c,其中(x1, y1)和(x2, y2)表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上c。
请你将进行完所有操作后的矩阵输出。
输入格式
第一行包含整数n,m,q。
接下来n行,每行包含m个整数,表示整数矩阵。
接下来q行,每行包含5个整数x1, y1, x2, y2, c,表示一个操作。
输出格式
共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。
数据范围
1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
-1000≤c≤1000,
-1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例:
2 3 4 1
4 3 4 1
2 2 2 2
给定原矩阵a[i,j],构造差分矩阵 b[i,j],使得a[][]是b[][]的二维前缀和
差分核心操作:给以(x1,y1)为左上角,(x2,y2)为右下角的子矩阵的所有数a[i,j] ,加上C 【包含这个格子的大矩阵都被影响要+C】
对于差分数组的影响:
b[x1][y1] += c;//x1,y1之前的矩阵不受影响,到了[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] ; b[i][j]用 :insert(i,j,i,j,a[i][j]); (从1开始)
**二维差分:**
b[x1][y1] += c
b[x2 + 1][y1] -= c
b[x1][y2 + 1] -= c
b[x2 + 1][y2 + 1] += c
算出新的二维前缀和数组:
a[i][j] = a[i-1][j] + a[i][j-1] - a[i-1][j-1] + b[i][j];
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],b[N][N];
//差分操作
void insert(int x1,int y1,int x2,int y2,int c) {
b[x1][y1] += c;//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 main() {
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]); //初始化b[i][j]
}
}
//询问
while(q--) { //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++) {//差分数组b的前缀和数组为: a
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;
}
双指针
【先想朴素暴力解法,然后看能否改成双指针】
①双重循环遍历 (i、j指针) 【最核心操作】
for (int i = 0,j = 0;i < n;i++)
while( j < i && check(i,j)) j++②维护一个区间 (l、r指针)
核心思想【优化】:用某种性质, 把O(n2)的算法优化成O(n)
1)对于一个序列,用两个指针维护一段区间
2)对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
把每一个单词单独输出(空格跳过)
#include <iostream>
using namespace std;
#include<cstring>
//把每一个单词单独输出(空格跳过)
int main() {
char str[1000];
gets(str);
int n = strlen(str);
for(int i = 0; str[i]; i++) {
int j = i;
while(j < n && str[j] != ' ') j++; //j用于指向空格
//这道题的逻辑
for( int k = i; k < j; k++) cout << str[k]; //单独输出每个单词
cout << endl;
i = j; //指向空格,最后再i++,则i指向下一个单词首字母 【双指针特性优化!保证O(n)】
}
return 0;
}
leetcode讲解Java版代码,但leetcode此题没有求左右端点下标
f[i]=max(f[i-1]+a[i],a[i])=max(f[i-1],0)+a[i];
最大连续子序列的和
非完整讲的不错
799. 最长连续不重复子序列
给定一个长度为n的整数序列,请找出最长的不包含重复数字的连续区间,输出它的长度。
输入格式
第一行包含整数n。
第二行包含n个整数(均在0~100000范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复数字的连续子序列的长度。
数据范围
1≤n≤100000
输入样例:(单调性!才优化)
5
1 2 2 3 5
输出样例:连续最长(2,3,5)
3
朴素算法O(n2)
for(int i = 0;i < n;i++)
for(int j = 0; j <= i; j++)
if(check(i,j))
{
res = max(res,j,i - 1);
}
序列单调性,双指针优化 (大的区间不重复,小的区间也不重复, j左(往左最远可以到哪里)、i右一直往前移即可)
【先想朴素暴力解法,然后看能否改成双指针】
如for(i = 0,j = 0;i < n;i++) for (i = 0,j = 0;i < n;i++) 双重循环 if(check(条件))
改写双指针: for(i = 0,j = 0;i < n;i++){ while(check(j)) }
简记:
s数组标记出现元素个数:while(s[a[i]]>1) s[a[j]] -- ; j++ ; 把j走过的元素数量减去
res = max(res , i - j + 1);
#include<bits/stdc++.h>
using namespace std;
#include<algorithm>
int main()
{
const int N = 100010;
int n,a[N],s[N];//s标记出现个数
cin >> n;
for(int i = 0;i < n; i++) cin >> a[i];
int res = 0;
for(int i = 0,j = 0;i < n;i++) //j往左走最远能到什么地方(j<i)
{
s[a[i]] ++ ;//存值标记数组,已有就说明重复
while(s[a[i]] > 1) //有重复
{
s[a[j]] --;//减去j存放的值(数量)下标
j ++;//j向左移动(直到不包括重复元素),等效i的位置不满足回退,但不用i回退只需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)。
数据保证有唯一解。
输入格式
第一行包含三个整数n,m,x,分别表示A的长度,B的长度以及目标值x。
第二行包含n个整数,表示数组A。
第三行包含m个整数,表示数组B。
输出格式
共一行,包含两个整数 i 和 j。
数据范围
数组长度不超过100000。
同一数组内元素各不相同。
1≤数组元素≤109
输入样例: (长度1 长度2 x)
4 5 6
1 2 4 7
3 4 6 8 9
输出样例:(下标从0开始)
1 1
①先想暴力解法:
for n
for m
if(A[i] + B[j] == x ){ 输出答案 ;break;}
②单调性优化
此题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 main() {
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++) {
while(j >= 0 && a[i] + b[j] > x) j--; //已知单调性,最大递减寻找两个数为目标和,复杂度O(n)
if(a[i] + b[i] == x) {
printf("%d %d\n",i,j);
break;
}
}
return 0;
}
位运算
记:【最常用n >> k & 1 和 lowbit(x) :x & -x 最后一位1的位置的值】
n的二进制中表示第k位的变化
n = 15 = 0b(1111)
①先把第k位移到最后一位,n >> k
②看n的个位是几 : n & 1
③(①+ ②并用) 计算出的值:n的第k位是0/1 : n >> k & 1 【先移动后与1】
lowbit(x) 返回x的最后一位1的位置 ,如 x = 1010 , lowbit(x) = 10
lowbit其实就是 x & -x == x & (~x + 1) ,【-x是x的补码 = ~x + 1】
补码:用加法做减法 :
x + (-x) = 0 , -x = 0 - x (用32位0,不够减向前面借一个1,【第33位的1】,去减x ;等效 ~x + 1)
2n 等价【1 << n 】
int main()
{
int n = 10;
for(int k = 3;k >= 0;k --) cout << (n >> k & 1); //第k位是几
cout << endl;
unsigned int x = -n;
for(int i = 31;i >= 0;i--) cout << (x >> i & 1); //输出补码
cout << endl;
return 0;
}
801.二进制中1的个数
题目描述
给定一个长度为n的数列,输出数组中每个数的二进制中1的个数
1 <= n <=100000;
0 <= 数量中的值 <= 109^
输入样例:
5
1 2 3 4 5
输出样例:
1 1 2 1 2
简记:每次减去最后一个1 : lowbit(x) return x&-x ,
while(x) x-=lowbit(x); ans++; 每次减去最后一位1,统计1的个数 ,减到没有1(全0),x==0
int lowbit(int x)//x的最后一位1 的值
{
return x & -x;
}
int main()
{
int n,res;
cin >> n;
while(n --)
{
int x;
cin >> x;
int res = 0;
while(x) x -= lowbit(x),res++; //每次减去最后一位1,统计1的个数 ,减到没有1(全0),x==0
cout << res << " ";
}
return 0;
}
扩:
#include<algorithm>
#include<vector>
vector<int> alls; //存储所有待离散化的值
sort(alls.begin(),alls.end()); //将所有值排序
alls.erase(unique(alls.begin(),alls.end()) , alls.end() ); //重复的元素会被整理到后面,删除这段重复的元素就可以了
algorithm 的头文件
lower_bound
upper_bound
binary_search
find
从小到大
函数lower_bound()在first和last中的前闭后开区间进行二分查找,返回大于或等于val的第一个元素位置。如果所有元素都小于val,则返回last的位置,且last的位置是越界的
返回查找元素的第一个可安插位置,也就是“元素值>=查找值”的第一个元素的位置
binary_search
802. 区间和 (离散化)[知识点多]
[离散化需理解 ,不完整… 区间合并,需改进]
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含两个整数 x 和 c。
再接下来 m 行,每行包含两个整数 l 和 r。
输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。
数据范围
-109≤x≤109,
1≤n,m≤105,
-109≤l≤r≤109,
-10000≤c≤10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5
操作1.如果离散化的值为k,则a[k] += c
操作2.询问左右边界 最多用3* 105 个数(坐标) --> 离散化,映射到从1开始的自然数
离散化(整数)
题目值域 0~109 ,但开不了这么大的数组
如值映射到另一个数组的下标 ,按顺序把数存到离散化数组(按顺序存题目数据的过程 – 离散化)中
【a是有序的】a[i]中可能有重复元素 去重
如何算出a[i]中离散化后的值 二分
简记:【全部记】
模板 //alls存放了要用到的下标
sort(alls.begin(),alls.end()); //将所有值排序
alls.erase(unique(alls.begin(),alls.end()) , alls.end() ); //重复的元素会被整理到后面,删除这段重复的元素就可以了
返回值r+1是依据题目从1开始映射,不加1则从0开始
int find(int x)//找到大于等于第一个x的位置 + 1
{
int l = 0,r = alls.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 【从1开始映射】,前缀和 从1开始
}
#include<bits/stdc++.h>
using namespace std;
#include<vector>
const int N = 300010;
typedef pair<int,int > PII;
int n,m;
int a[N],s[N];
vector<int> alls;
vector<PII> add,query; //add存插入操作,query存询问操作
//二分
int find(int x)//找到大于等于第一个x的位置 + 1 ,(依据题目从1开始映射) 添加存放离散化
{
int l = 0,r = alls.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 【从1开始映射】,前缀和 从1开始
}
int main() //插入操作存1个坐标,n次10^5^ + 查询操作存2个坐标 ,2m次 2* 10^5^ > 3 * 10^5^
{
cin >> n >> m;
for(int i = 0;i < n;i++)
{
int x,c;
cin >> x >> c;
add.push_back({x,c}); //读入插入操作
alls.push_back(x);//加入待离散化的数组中
}
for(int i = 0;i < m;i++)
{
int l , r;
cin >> l >> r;
query.push_back({l,r});
alls.push_back(l);//区间的左右端点都是需要离散化的,加入待离散化数组
alls.push_back(r);//存放了要用到的下标
}
//去重
sort(alls.begin(),alls.end()); //将所有值排序
alls.erase(unique(alls.begin(),alls.end()) , alls.end() ); //重复的元素会被整理到后面,删除这段重复的元素就可以了
//插入操作
for(auto item : add)
{
int x = find(item.first);//离散化之后的值(映射下标)
a[x] += item.second;//离散化位置加对应数值
}
//预处理前缀和 【从1开始】
for(int i = 1;i <= alls.size();i++) s[i] = s[i - 1] + a[i]; //alls.size()为最大个数下标
//处理询问
for(auto item : query ) //query存了询问区间[l,r] query.first,query
{
int l = find(item.first),r = find(item.second); //左右区间离散化的下标
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
803. 区间合并
【多种办法,此处离散化】
题目:
给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含两个整数 l 和 r。
输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。
数据范围
1≤n≤100000,
-109≤li≤ri≤109
输入样例:
5
1 2
2 4
5 6
7 8
7 9
输出样例:
3
思路:
有交集的部分合并,C总个数 = A + B - 交集个数
①按区间的左端点排序
②每次维护当前的区间 ,左端点简称st(start) 右端点简称ed(end) :
要合并的区间与当前区间的关系:
1)为子集(包含) 取大的集合作为答案
2)有交集 补充不相交的元素
3)没有交集 确定一个区间不会再和其他区间有交集,就放到答案里面,更新维护区间为没有交集的区间
最后的答案为:一系列没有交集的区间
简记:【背】segs存合并后每个区间左右端点 {l,r} ,每次segs = res
void merge(vector<PII> &segs){
vector<PII> res;//遍历用,返回答案数组赋值它的地址给segs
sort(segs.begin(),segs.end());//先按l排序
int st = -2e9 , ed = -2e9;//初始边界 负无穷
for(auto seg : segs)//按左端点排序后遍历【seg当前区间】
if(ed < seg.first) //维护区间r=ed都在当前区间的左边[区间没有交集]
{
if(st != -2e9) res.push_back({st,ed}); //st代表不能是最开始,即初始区间(为空) ,满足非空,就把[l,r]放入答案返回区间res
st = seg.first , ed = seg.second;//维护区间更新成当前的(图)
}
else ed = max(ed,seg.second); //st=l不变,判断ed,当前的区间和维护的区间有交集,取较长的(取右端点大的)
if(st != -2e9) res.push_back({st,ed}); //最后得到的这个区间加入答案 [st != -2e9]防止数组没有区间,为空
segs = res;
}
/*
按区间左端点排序
seg扫描过程中维护一个当前的区间 :当前l = seg.first , r = seg.second
[st,ed] 三种情况取并集
①【包含则不变】
②【部分并集】
③【没有交集】:把此区间加入答案,更新成st,ed 为此区间的[l,r]
过程中ed < l ,则都是情况③,没有交集,更新
*/
#include<bits/stdc++.h>
#include <algorithm>
#include<vector>
using namespace std;
typedef pair<int,int > PII;
const int N = 100010;
int n;
vector<PII> segs;
//存入了最后合并的各个区间[l,r] for可求max区间长度 ,同时区间个数可以用segs.size()得出
void merge(vector<PII> &segs)
{
vector<PII> res;//遍历用,返回答案数组赋值它的地址给segs
sort(segs.begin(),segs.end());//先按l排序
int st = -2e9 , ed = -2e9;//边界 负无穷
for(auto seg : segs)//按左端点排序后遍历【seg当前区间】
if(ed < seg.first) //维护区间r=ed都在当前区间的左边[区间还没有交集]
{
if(st != -2e9) res.push_back({st,ed}); //st代表不能是最开始初始区间(非空) ,满足非初始非空,即放入答案区间
st = seg.first , ed = seg.second;//更新成左右区间
}
else ed = max(ed,seg.second); //st=l不变,判断ed,当前的区间和维护的区间有交集,取较长的(取右端点大的)
if(st != -2e9) res.push_back({st,ed}); //最后得到的这个区间加入答案 [st != -2e9]防止数组没有区间,为空
segs = res;
}
int main() {
cin >> n;
for(int i = 0;i < n;i++)//读入端点,存segs二元数组中
{
int l,r;
cin >> l >> r;
segs.push_back({l,r});
}
merge(segs);
cout << segs.size() << endl; //完成后的区间个数
return 0;
}
if(res.size()>1)
printf("no");
else
printf("%d %d",res[0].first,res[0].second);
扩题:759.考察离散化
week1习题讲解
791-803均已讲解完
786.求第k个数
给定一个长度为n的整数数列,以及一个整数k,请用快速选择算法求出数列的第k小的数是多少。
输入格式
第一行包含两个整数 n 和 k。第二行包含 n 个整数(所有整数均在1~109范围内),表示整数数列。
输出格式
输出一个整数,表示数列的第k小数。
数据范围
1≤n≤100000,1≤k≤n
输入样例:
5 3
2 4 1 5 3
输出样例:
3
简单法:排序完,取下标
#include<iostream>
using namespace std;
void quickSort(int f[], int l, int r) {
if(l >= r) return;
int i = l - 1, j = r + 1, x = f[l + r >> 1];
while(i < j) {
do i++; while(f[i] < x);
do j--; while(f[j] > x);
if(i < j) swap(f[i], f[j]);
}
quickSort(f, l, j);
quickSort(f, j+1, r);
}
int main() {
int n, k;
scanf("%d", &n);
scanf("%d", &k);
int f[n];
for(int i = 0; i < n; i++) scanf("%d", &f[i]);
quickSort(f, 0, n-1);
printf("%d", f[k-1]);
return 0;
}
利用快速选择算法
1.找到分界点x,q[L],q[(L + R) / 2] , q[R]
2.左边所有数left <= x,右边所有数 >= x
3.递归排序left,递归排序right
①k <= sl(S-left-length) ,递归left(第k个在左边)
②k > sl ,(递归right)
记:j为递归分界下标,相当于第j大的个数,
前半部分sl长度为 j-L+1 个数(从小到大个数),若k<=sl,则在k在[L,j]中
k>sl,则第k个数在[j+1,r]中的 k-sl 个 ,写成递归[j+1,k-sl]
【最终答案仅在递归中最后落到的数l==r时,答案写递归出口return q[l]或q[r] 但是快排出口还有l>r的可能,所以 if(l>=r) return q[L] 】
#include <iostream>
using namespace std;
const int N = 100010; //多10 比如 防止数组下标越界
int n,k;
int q[N];
int quick_sort(int l,int r,int k){
if(l >= r) return q[l]; //找到 快排必须写 >= 因为最终可能没有数满足
int x = q[l], i = l - 1,j = r + 1; //快排边界区间在两侧
while(i < j)
{
while(q[++i] < x); //照顾不会do-while的同学
while(q[--j] > x);
if(i < j) swap(q[i] , q[j]);
}
int sl = j - l + 1;
if(k <= sl) return quick_sort(l,j,k); //第k个数在左边区间
return quick_sort(j + 1,r,k - sl);//k - sl为右区间的第几个
}
/* n k 数组
5 3
2 4 1 5 3
*/
int main(){
cin >> n >> k;
for(int i = 0;i < n;i++){
cin >> q[i];
}
cout << quick_sort(0,n - 1,k) << endl; //区间中第k大的数
return 0;
}
788.逆序对的数量
题目:
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。
输入格式
第一行包含整数 n,表示数列的长度。
第二行包含 n 个整数,表示整个数列。
输出格式
输出一个整数,表示逆序对的个数。
数据范围
1≤n≤100000 (极限 全部逆序 , n + n - 1 + … + 1 = n * (n - 1) / 2 == 1012 / 2 > int 232 -->long long)
输入样例:
6
2 3 4 5 6 1
输出样例:
5
定义:每两个数比较 , 前面的数比后面的数大 , 则为一个逆序对
归并排序:
- [L,R] => [L,mid],[mid + 1,R]
- 归并排序 [L,mid] 和 [mid + 1, R]
3.归并排序 ,将左右两个有序序列合并成一个有序序列
逆序对比较三种情况 :
①都在左半边 merge_sort(L,mid)
②一个左半边,一个右半边
③都在右半边 merge_sort(mid+1,r)
简记: 【仅merge_sort中添加res += mid - i + 1】
while(i <= mid && j <= r){
if(q[i] <= q[j]) tmp[k++] = q[i++];
else{
tmp[k++] = q[j++];
res += mid - i + 1; //分别在左右边已经排好序的情况③,当大于1个数即比这个数后面的数都大的区间 res要 += 此情况的逆序对个数
typedef long long LL;
int n;
int q[N],tmp[N];
LL merge_sort(int l,int r){
if(l >= r) return 0;
int mid = l + r >> 1;
LL res = merge_sort(l,mid) + merge_sort(mid+1,r); //排序结果相加
//归并的过程
int k = 0,i = l,j = mid + 1;//两个指针 头 和 中间 -->分段比较排序 -- 小的放前面一段
while(i <= mid && j <= r){
if(q[i] <= q[j]) tmp[k++] = q[i++];
else{
tmp[k++] = q[j++];
res += mid - i + 1; //分别在左右边已经排好序的情况③,当大于1个数即比这个数后面的数都大的区间 res要 += 此情况的逆序对个数
}
//扫尾 (没遍历完的接在后面)
while(i <= mid) tmp[k++] = q[i++];
while(j <= r) tmp[k++] = q[j++];
}
//物归原主
for(int i = l, j = 0;i <= r;i++,j++){ //q_02数组的区间 [l,r] tmp_02[j]从0开始按位置放q_02[i]
q[i] = tmp[j];
}
return res;
}
int main(){
cin >> n;
for(int i = 0;i < n;i++) cin >> q[i];
cout << merge_sort(0 , n - 1) << endl;
return 0;
}
790.数的三次方根
给定一个浮点数n,求它的三次方根。
输入格式
共一行,包含一个浮点数n。
输出格式
共一行,包含一个浮点数,表示问题的解。
注意,结果保留6位小数。
数据范围
-10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000
简记:浮点二分:都是mid
int main(){
double x;
cin >> x;
double l = -10000,r = 10000;
while(r - l > 1e-8)
{
double mid = (l + r) >> 1;
if(mid * mid * mid >= x) r = mid;
else l = mid ;
}
printf("%lf\n",l);//lf默认保留6位小数 ,也可以加 .6
return 0;
}
一维前缀和:
1 . Si = a1 + a2 + … + ai
2. sum(L,R) = aL + aL+1 + aL+2 + … + aR = S[k] - S[i-1]
1.预处理前缀和数组
2.用公式求区间和
重要总结 :凡是数组有用到下标i-1的都从1开始定义 ! dp / hash / dfs等
数据结构
#include <iostream>
using namespace std;
/*
用数组模拟动态链表的原因:每次new节点(结构体)需要一定耗时
1.链表与邻接表
2.栈与队列
3.kmp
*/
826.单链表
实现一个单链表,链表初始为空,支持三种操作:
(1) 向链表头插入一个数;
(2) 删除第k个插入的数后面的数;
(3) 在第k个插入的数后插入一个数
现在要对该链表进行M次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第k个插入的数并不是指当前链表的第k个数。例如操作过程中一共插入了n个数,则按照插入的时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,…第n个插入的数。
输入格式
第一行包含整数M,表示操作次数。
接下来M行,每行包含一个操作命令,操作命令可能为以下几种:
(1) “H x”,表示向链表头插入一个数x。
(2) “D k”,表示删除第k个输入的数后面的数(当k为0时,表示删除头结点)。
(3) “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
【new node 非常慢! 所以一般竞赛、面试也用数组模拟,而不是用指针】
[ typedef struct node{e , ne ,idx} , node[N];结构体数组模拟结点,但是代码变长,仍不是最优写法 ]
记含义:
算法题:追求速度
head 表示存头结点的下标
e[i] 表示下标节点i的值
ne[i] 表示节点i的next指针,值为指向下一个结点值是多少
idx 存储当前已经用到了哪个点(第k个插入的数idx = k-1) (数组长度)
【head-> 3-> 5 -> 7-> 9-> NULL】
head表示头结点下标,刚开始初始化指向-1 , head(null) 还没有指向 (-1表示空)
e[0] = 3 (第一个节点,下标0,地址存放的值)
ne[0]= ② [顺序为链表的第二个结点,结点下标(地址)]
空集用ne[最后结点的next]=-1表示, e和ne用下标关联
idx存储当前用到的地址(相当于一个指针,从第一个结点开始)
插入:),赋x值,元素x指向head->next ,然后头结点指向新结点(数组模拟的lead的next为ne[0] = idx(下一个结点下标)) ,head(指针)指向此插入元素x(元素的结点对应下标idx,再idx++
①初始化 (头指针)head = -1(空) ; idx = 0(当前无结点,0为第一个插入的结点下标);
②【头插法-算法题80%】e[idx] = x ; ne[idx] = head ; head = idx ; idx++ ;
③将x插到下标是k的点【后面】 (在下标k结点前面插入就传入实参k-1)
e[idx]=x ; ne[idx] = ne[k]; ne[k] = idx ;idx++;
④将下标k后面的节点删除 (删除第k个的结点就传入实参k-1)
ne[k] = ne[ne[k]]; //递归的味道
⑤数组模拟链表循环遍历(i模拟指针从head开始沿着ne[]):
for(int i = head ;i != -1;i = ne[i])
const int N = 100010;
int head,e[N],ne[N],idx;
//初始化
void init() {
head = -1;
idx = 0;
}
//将x插入到头结点
void add_to_head(int x) { //插入值存入 这个插入节点的存放值的区域
e[idx] = x;
ne[idx] = head; //当前节点idx的next指针 存节点(指向头结点)
head = idx; //head的next指向idx位置 (head不存值,所以head本身值代表next位置(存放头结点下标位置))
idx++; //节点用过了,数组中移动到下一个位置
}
//将x插到下标是k的点【后面】 (插入下标k结点前面就传实参k-1即可)
void add(int k,int x) {
e[idx] = x;
ne[idx] = ne[k];//idx的next指向k的下一位
ne[k] = idx;
idx++; //注意:数组中使用过就不再用,移位 【同时代表长度】
}
//将下标k后面的节点删除(删除第k个的结点就传入实参k-1)
void remove(int k) {
ne[k] = ne[ne[k]]; // k->next = k->next->next (等效删除)
}//对应算法题可以不用管idx 【这里会浪费空间(k+1的位置),且idx不能表示长度了 -- 题目不需要用到长度,就没关系】
int main() {
int m;
cin >> m;
init();
while(m--) {
int k,x;
char op;
cin >> op;//cin会过滤空格等,scanf输入字符串可能出错
if(op == 'H') {
cin >> x;
add_to_head(x);
} else if(op == 'D') {
cin >> k; //若无处理,则删除头结点操作无效
if(!k) head = ne[head]; //k == 0时 删除头结点 head指向头结点的下一个节点嵌套ne[存头结点位置head]
remove(k - 1); //delete第k个点
} else {
cin >> k >> x; //题目是在第k个位置操作...而函数定义操作k后面的数 ,
add(k - 1,x); //用k - 1
}
}
for(int i = head; i != -1; i = ne[i]) cout << e[i] << " "; //这种输出结束条件为 i != -1 ,-1为最后的节点next->NULL的值
cout << endl;
return 0;
}
827.双链表
实现一个双链表,双链表初始为空,支持5种操作:
(1) 在最左侧插入一个数;
(2) 在最右侧插入一个数;
(3) 将第k个插入的数删除;
(4) 在第k个插入的数左侧插入一个数;
(5) 在第k个插入的数右侧插入一个数
现在要对该链表进行M次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第k个插入的数并不是指当前链表的第k个数。例如操作过程中一共插入了n个数,则按照插入的时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,…第n个插入的数。
输入格式
第一行包含整数M,表示操作次数。
接下来M行,每行包含一个操作命令,操作命令可能为以下几种:
(1) “L x”,表示在链表的最左端插入数x。
(2) “R x”,表示在链表的最右端插入数x。
(3) “D k”,表示将第k个插入的数删除。
(4) “IL k x”,表示在第k个插入的数左侧插入一个数。
(5) “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
双链表不用head【区别于单链表】
因为一般算法题中,单链表的head要用来存一个有效数据(指向下一个结点的地址),
而双链表不用,所以头尾(左右端点)用0 ,1 表示即可【为算法题服务的最常用模板】
记:
L[0](头)= 0,R[0] = 1(尾) //初始化左右指针
【右指针往左走,初始化在最右端,右端点记为idx=1
左指针往右走,初始化在最左端,左端点记为idx = 0】
删除第k个点 R[L[k]] =L[k] ; L[R[k]] = R[k]
(让第k结点的左指针指向右指针的下一个结点下标, k的右指针指向左指针指向的下一个节点的下标地址,
此时k脱离链表,没有指针指向k)
const int N = 100010;
int m;
int e[N],l[N],r[N],idx; //l->next(前驱) , r->next (后继) e存元素值 ,idx当前结点
//初始化 (两个无值的边界结点)
void init(){
//0表示左端点,1表示右端点
l[1] = 0,r[0] = 1; //初始化,右指针r在最右端,l在最左端,左端点idx=0
idx = 2; //注意0,1已经用过了,idx初始值为2
}
//在下标是k的点的右边,插入x
//①idx右 = k右 ②idx左 = k (先③k右(即k+1)的左 = idx) (后④k右 = idx)
void add(int k,int x) // *在k的左边插入x,传入参数k-1 , 就调用参数为 l[k] == (k - 1)
{
e[idx] = x;//赋值
r[idx] = r[k]; // idx.r->next = k.r->next idx右边指向k+1 == k.r->next
l[idx] = k; //idx.l->next = k idx左边指向k
l[r[k]] = idx; //k.r->next->l->next = (k+1).l->next = idx
r[k] = idx; //k.r->next = idx
}
//删除第k个点
void remove()
{
r[l[k]] = r[k]; //k.l->next->r->next(即(k-1).r->next) = k.r->next = (k+1)
l[r[k]] = l[k]; //同理(k+1).l->next = k.l->next = (k-1)
}
int main()
{
cin>>m;
init();
while(m--)
{
int k,int x;
string op; cin>>op;
if(op=="L")
{
cin>>x;
add(0,x);
}
if(op=="R")
{
cin>>x;
add(l[1],x);
}
if(op=="D")
{
cin>>x;
remove(x+1);
}
if(op=="IL")
{
cin >> k >> x ;
add(l[k+1],x);
}
if(op=="IR")
{
int k,x; cin>>k>>x;
add(k+1,x);
}
}
for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";
return 0;
}
/*栈
const int N = 100010
int stk[N] , tt;
//入栈
stk[++tt ] = x;
if(tt > 0) not empty
else empty
//弹出
tt-- ;
*/
/*队列
int q[N] ,hh,tt = -1;
//插入队尾
q[ ++tt ] = x;
//队头弹出
hh++;
//判断是否为空
if(hh < tt ) not empty
else empty
//取队头元素
q[hh]
单调栈(抽象但题型很少) 【左边第一个较小的数】
【单调栈/队列-最常解决的问题:给定序列,求每一个数的左边的离它最近的比它大/小的数(满足某种条件)
单调栈 (单调上升)
题目描述 :
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 -1。
输入输出 :
输入
5
3 4 2 7 5
输出
-1 3 -1 2 2
此题暴力 for (int i = 0;i <=n;i++) for(j = i - 1;j>=0;j–){找到左边第一个小于自身的元素;break} // j开始指向左边第一个元素,开始判断
【找左边的第一个小的元素,就把比右边大的元素删掉(因为有更小的数),肯定不是答案-没有用 - 得到单调栈 】
栈和队列里的存的不是元素值,而是元素下标
记:(在栈顶插入,弹出)
【插入】stk[++ tt] = x ;
【弹出删除】tt - - ;
【判断是否为空】if(tt > 0) not empty ; else empty;
【取栈顶】stk[tt] ; (tt初始值0,第一个入栈元素下标为1)
const int N = 100010;
int n;
int stk[N],tt;
int main()
{
//cin.tie(0); 加速cin读取
//ios::sync_with_stdio(false);
//cin >> n;
scanf("%d",&n);
for(int i = 0;i < n;i++) //每个元素最多出栈一次,进栈一次:2n, 复杂度O(n)
{
int x;
scanf("%d",&x);
while(tt && stk[tt] >= x ) tt--; //保证单调性,大的数放在前面
if(tt) cout << stk[tt] << " "; //栈不为空 (遍历到最后) tt != 0时打印
else cout << -1 << " "; //栈空了还未找到
stk[ ++tt] = x; //加入x,保证前面的数小于x
}
return 0;
}
单调队列 【滑动窗口】
滑动窗口
问题描述:
给定一个大小为n ≤ 106的数组。
有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到k个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为[1 3 -1 -3 5 3 6 7],k为3。
窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7
输入格式:
输入包含两行。
第一行包含两个整数n nn和k kk,分别代表数组长度和滑动窗口的长度。
第二行有n nn个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式:
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
数据范围:
1 ≤ n , k ≤ 100000
输入样例:(元素长度,窗口大小)
8 3
1 3 -1 -3 5 3 6 7
输出样例:(第一行输出最小值,第二行输出最大值【窗口大小为3,即3个中的最小】)
-1 -3 -3 -3 3 3
3 3 5 5 6 7
每次滑动一格 : 【一进一出】队尾加进一个新元素,队头出队一个元素
q[tt]存放a数组的下标,取值用a[q[tt]] ,因此数组排序不变,队列可以循环使用数组判断
做题:
①先朴素算法模拟 :一进一出维护窗口大小k , 遍历窗口O(k)找最大最小
②优化:把没有用的元素删去,看是否具有单调性【最大最小值分开求】 (最小值:前面的数比后面的数大,则前面一定没有用,删掉,则变成单调递增,最小值一定在窗口的最左边,头指针)
最大值相反,构造单调递减,删去比前面大的元素,此时队头为最大值
记:(在队尾插入元素,在队头弹出元素)
int q[tt],hh,tt = -1; //初始值个人习惯
【插入】q[++tt] = x ;
【弹出】hh++ ;
【判断是否为空】if(hh <= tt) empty ; else empty;
【取队头元素】q[hh] , 队尾也可以取:q[tt]
const int N = 1000010;
int a[N],q[N];
int n,k;
int main(){
scanf("%d%d",&n,&k);
for(int i = 0;i < n;i++) scanf("%d",&a[i]);
//最小值
int hh = 0,tt = -1;
for(int i = 0;i < n;i++)
{
//判断对头是否已经滑出窗口
if(hh <= tt && i - k + 1 > q[hh]) hh++; //队列不为空 && 值
while(hh <= tt && a[q[tt]] >= a[i]) tt--; //循环窗口内值比较
q[++tt] = i; //存答案的下标
if(i >= k - 1) printf("%d ",a[q[hh]]);
}
puts("");
//求最大值 - 从单调上升变成单调递减 条件改成 a[q[tt]] <= a[i]
hh = 0,tt = -1;
for(int i = 0;i < n;i++)
{
//判断对头是否已经滑出窗口(hh头指针-->向右滑动)
if(hh <= tt && i - k + 1 > q[hh]) hh++; //队列不为空 && 窗口位置不越界 滑到 a[n-k-1]为最后一次滑动 【弹出,向右滑动】
while(hh <= tt && a[q[tt]] <= a[i]) tt--;//找最大 ,单调递减队列
//循环窗口内值比较
q[++tt] = i; //存答案的下标 【入队,向右滑动】
if(i >= k - 1) printf("%d ",a[q[hh]]);
}
puts("");
return 0;
}
kmp
给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串 P 在模式串 S 中多次作为子串出现。
求出模板串 P 在模式串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1≤N≤105
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2
先想
1.暴力(朴素)做法:s短串,p串 ,双重循环 for(int i = 0;i < n;i++) bool flag = true;
for(j = 1;j <= m;j++) {if(s[i] != p[j]) 如果不相等 flag = false ,break退出}
记:【p短n,s长m ,从1开始存 ,下标位于 [1,n] 和 [1,m] 】
【具体看代码:next数组过程与kmp非常类似,代码仅把s[i]换成自身p[i]即可】
【ne数组构造过程和kmp匹配过程类似,不相同不断回退:
for(i = 2,j = 0;i < n;i++),j=ne[j]//i = 1第一位失败不用退不用退,若判断if(q[i]==q[j+1]相等) j++ ,
, ne数组存放相同的前后缀长度(即回退下标),循环每轮求得 ne[i] = j】
【kmp过程】
模板串自身前缀相等的位置标记,在匹配失败回退时,只需退到相等前缀位置
再次判断下一位,不行再退j = ne[j]
for(int i = 1,j= 0 ; i <= m ; i ++ )
{
while( j&& s[i] != p[j + 1] ) j = ne[j] //退无可退 或者不匹配,p依据前后缀回退
if( s[i] == p[j + 1] ) j++; // i随着循环增加 , 比较下一位if(j == n) //匹配成功
{按题目要求}
}
预处理出数组next数组 :
子串匹配失败时,要至少向后移动多少,才能和主串(已经匹配过的位置)匹配上
前面的位置和匹配失败的 到移动的位置与主串对应位相等 ,同时也与之前已经匹配的位置相等(等效)
模板串自身的后缀和前缀相等的长度最大值==即可以回溯的位置
用next[j] : 逐位匹配的下一位判断,若 s[i] != p[j+1] 时,j回退到next[j],next[j]数组维护前后缀的最长相等长度
即指向匹配模式串的j指针在不相等时,依据ne[j]数组不断回退,如果回退到0,即从0开始(退回模式串起点)
#include<bits/stdc++.h>
using namespace std;
/*字符串匹配BF
暴力+剪枝优化 朴素解法
int s[N],p[N];//s较短串,p较长串
for(int i = 0;i < n;i++)
{
bool flag = true;//是否成功
for(int j = 1;j <= m;j++)
{
if(s[i] != p[j])
{
flag = false;
break;
}
}
}
*/
const int N = 10010,M = 100010;
int n,m;
char p[N],s[N];//模板主串,模式串 【p在s(长)中寻找匹配的片段】
int ne[N];//回溯数组(最长前后缀数组)
int main()
{ //p长度n ,s长度m
cin >> n >> p + 1 >> m >> s + 1; // 模板主串长度 ,输入主串**(下标从1开始)** ,模式串长度 ,输入模式串(从1开始)
//求next[j]过程(类似kmp匹配的过程) //i = 1的时候没有前后缀,只有一个元素 ,不能包括自己(不用回退,i=2开始)
for(int i = 2,j = 0;i <= n;i ++)
{ //【回退条件:(j == 0不能再退了,最终等待i循环完退出) && (不匹配时需回退)
while( j && p[i] != p[j + 1]) j = ne[j]; //【条件和kmp匹配过程类似,自身找到相同第一个位置,j=ne[j],再判断if(相等) j++ , ne数组存放相同的前后缀长度,循环每轮求得 ne[i] = j】
//若j = 0就从头开始,不用回退了,执行判断语句即可
if(p[i] == p[j + 1]) j++;
ne[i] = j;//第i个位置的最长前后缀相等长度 ,长度为j
}
//kmp匹配过程 O(n),只看一个变量判断复杂度,i是m次(while循环j回溯等效至少每次减1,即最多执行m次) // s(长)为 m
for(int i = 1,j = 0;i <= m;i++)//下标i=1开始匹配 ,next的j = 0开始存储回退下标next[0] = 0, next[1] = 0
{ //【回退条件:(j != 0) && (不匹配时判断回退位置)
while(j && s[i] != p[j + 1]) j = ne[j]; //若j = 0就从头开始,不用回退了,执行判断语句即可
if( s[i] == p[j + 1] ) j++;//模式串已经匹配到的长度位置
if(j == n)//匹配成功
{ //【此处按题目要求】输出(所有)匹配成功的主串的第一位的位置下标
//注意:我们存的是从1开始,但题目下标从0开始,所以第一位下标为i-n
printf("%d ",i - n); //输出匹配成功的主串第一位的位置 ,即主串当前位置i减去模式串长度n加上1得到第一位下标 ,即i - n + 1 ,再减去1 ,等效从0开始存储的下标值
j = ne[j];//题目要求所有位置,成功后回退到第一个位置 p[(j = 0) + 1]
//j得到回退值,新一轮从头开始移动,s下标i仍继续增加
}
}
return 0;
}
week2
800.数组元素的目标和(双指针)
给定两个**升序**排序的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0 开始。
请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
数据**保证有唯一解**。(才可以双指针优化)
输入格式
第一行包含三个整数 n,m,x,分别表示 A 的长度,B 的长度以及目标值 x。
第二行包含 n 个整数,表示数组 A。
第三行包含 m 个整数,表示数组 B。
输出格式
共一行,包含两个整数 i 和 j。
数据范围
数组长度不超过 105。
同一数组内元素各不相同。
1≤数组元素≤109
输入样例:
4 5 6
1 2 4 7
3 4 6 8 9
输出样例:
1 1
>【有单调性,唯一解:双指针】for(int i = 0 ,j = n - 1; i < n; i++ ) while(j >= 0 && A[i] + B[j] > x) j- -;
if( 目标元素和 == x) 输出目标元素下标i , j
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int a[N],b[N];
int n,m,x;
int main()
{
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++)
{
while(j >= 0 && a[i] + b[j] > x) j--;
if(a[i] + b[j] == x)
{
printf("%d %d\n",i,j);
break;
}
}
return 0;
}
802.区间和 (前缀和,离散化)
假定有一个无限长的数轴,数轴上每个坐标上的数都是 00。
现在,我们首先进行 nn 次操作,每次操作将某一位置 xx 上的数加 cc。
接下来,进行 mm 次询问,每个询问包含两个整数 ll 和 rr,你需要求出在区间 [l,r][l,r] 之间的所有数的和。
输入格式
第一行包含两个整数 nn 和 mm。
接下来 nn 行,每行包含两个整数 xx 和 cc。
再接下来 mm 行,每行包含两个整数 ll 和 rr。
输出格式
共 mm 行,每行输出一个询问中所求的区间内数字和。
数据范围
−109≤x≤109
1≤n,m≤105
−109 ≤ l ≤ r ≤109
−10000 ≤ c ≤ 10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5
【离散化:109 - - > 3 * 105 】
待定...
826.单链表 ; 827.双链表
Trie树
Trie树 :用来高效存储和查找字符串集合的数据结构
并查集
堆 (#include含有堆,但是不支持删除元素)
835. Trie字符串统计
维护一个字符串集合,支持两种操作:
“I x”向集合中插入一个字符串x;
“Q x”询问一个字符串在集合中出现了多少次。
共有N个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
输入格式
第一行包含整数N,表示操作数。
接下来N行,每行包含一个操作指令,指令为”I x”或”Q x”中的一种。
输出格式
对于每个询问指令”Q x”,都要输出一个整数作为结果,表示x在集合中出现的次数。
每个结果占一行。
数据范围
1≤N≤2*104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
题目一定限制字母的种类,如26个(较单一)
类似哈夫曼存储前缀编码
每一个分支打标记表示结尾 (查找的时候最后一位单词对应标记,若相等才可能找到)
记:【p=0沿着结点走下去,for(int i = 0;str[i];i++) //字母结尾下一位为0
{ int u = str[i] - ‘a’;//映射
if(!son[p][u]) son[p][u] = ++ idx; //下一个先加
p= son[p][u];
} 循环完单词插入cnt[p]++;
【p=0沿结点走下去,每轮插入完,p指向单词结尾当前结点idx,cnt[p]++】
【查询与插入神似,不用创造结点idx不变,p沿着寻找,找到return cnt[p]】
简记:
insert(char str[]):
循环串每位 for(int i = 0;str[i];i++){ 用str[i]判断是否到结尾 : '\0'
int u = str[i] - 'a'; 【相对位移量】 (a~z 映射成 0~25)
if(!son[p][u]) son[p][u] = ++ idx; 不断建造前缀
p = son[p][u]; p在建好的前缀串继续往下走,知道创建出得到这个串的路-
}
cnt[p]++;
query(char str[]): 与insert的 if(!son[p][u]) return 0;不同 (没有路,找不到就退出)
也不用cnt[p]++;
#include <iostream>
using namespace std;
const int N = 100010;
char str[N]; //单词
//下标0的点,既是根结点,又是空结点
//【比如】以x结尾的单词 ,son[x][儿子节点编号(单词)] 存结点下标
int son[N][26],cnt[N],idx; //son二维存放字典树(存每个结点的儿子), idx每个结点下标(每个字母一个下标)
// cnt为标记 【对应查找单词结尾有被标记才存在】
//题目给出只有小写字母,即每个节点最多向外连接26条边 , N为存放单词数量
void insert(char str[])
{
int p = 0;//根开始(根分支为字符串首字母)
for(int i = 0;str[i];i++) //用str[i]判断是否到结尾 == 0 时结束 ,遍历字符串,存入单词
{
int u = str[i] - 'a';//【相对位移量】 (a~z 映射成 0~25)
if(!son[p][u]) son[p][u] = ++ idx; //节点p的分支没有str[i] 的字母,无法构成前缀 ,没有路就建路,p继续往下走 (创建每个单个字母下标,组成前缀)
p = son[p][u]; //p=idx临时节点标号;cnt[p]++; ,同时
}
cnt[p] ++; //最后一个点【表示以这个点结尾的单词下标的数量(标记)】
}
int query(char str[])
{
int p = 0;
for(int i = 0;str[i];i++)
{
int u = str[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u]; //p沿着连续下标寻找,若是str[i]遍历完退出就找到,否则return 0
}
return cnt[p];
}
int main(){
int n;
scanf("%d",&n);
while(n--)
{
char op[2];
scanf("%s%s",op,str);
if(op[0] == 'I')insert(str);
else printf("%d\n",query(str));
}
return 0;
}
并查集【近乎O(1)】
1.将两个集合合并
2.询问两个集合是否在一个集合当中
基本原理:每个集合用一颗树表示。树根的编号就是整个集合的编号
对于每一个点都存储它的父节点编号(p[x]表示x的父节点),判断一个点是否属于集合,往父节点向上找,到树根的编号即为集合所在的编号
查询复杂度近乎O(1)
1.路径压缩【走过一遍,直接记录终点坐标】:find()递归查询后直接把节点值赋值为根节点值,直接对应集合
问题1.如何判断树根 if(p[x] == x)
问题2. 如何求x的集合编号 : while(p[x] != x) x = p[x]; //沿着父节点找
问题3.如何合并两个集合:px是x的集合编号,py是y的集合编号,p[x] = y
例如:【p[find(a)] = find(b)】
*常用面试题:代码短,思路
合并集合
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作共有两种:
“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
“Q a b”,询问编号为a和b的两个数是否在同一个集合中;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。
输出格式
对于每个询问指令”Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
暴力法遍历两个集合O(n) 用类似merge_sort写法
优化原理:同一个集合,某个元素找到根节点,其余元素就直接一步到根节点,
不用再沿着父节点遍历【路径压缩】
记:
每个点存储父节点,沿着父节点找根(一个根结点集合)while(p[x] != x) x = p[x];
合并【把一个集合的根节点当做另一个根节点的儿子】px是x的集合编号,py是y的集合编号,p[x] = y
p[find(x)] = find(y);
const int N = 100010;
int p[N];
int find(int x) //返回祖宗节点 + 路径压缩
{
if(p[x] != x) p[x] = find(p[x]); //递归找根节点 (集合的编号)
return p[x];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 0;i <= n;i++) p[i] = i; //先依题意构造集合
while(m--)
{
char op[2]; //两个操作
int a,b;
scanf("%s%d%d",op,&a,&b); //读字符串用%s可以忽略空格(如出题人在最后多打了一个空格)
if(op[0] = 'M') p[find(a)] = find(b); //合并集合,让一个集合的根节点的父节点为另一个集合的根节点
else
{
if(find(a) == find(b)) puts("Yes"); //在同一个集合里
else puts("No");
}
}
return 0;
}
837.连通块的数量 【并查集变型】
还有变型题:240.食物链(维护每个节点到根节点的距离)
题目描述:
给定一个包含 n个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m 个操作,操作共有三种:
C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
Q2 a,询问点 a 所在连通块中点的数量;
输入格式:
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为 C a b,Q1 a b 或 Q2 a 中的一种。
输出格式:
对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No。
对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量
每个结果占一行。
数据范围:
1≤n,m≤105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
连通块 :若A可以走到B,且B可以走到A,那么A、B连通
在不同的连通块之间连接一条边的时候,等效把两个集合合并
不同的是:多了一个操作,size表示每一个集合的大小(点的数量)
简记:
并查集多维护了一个size数组,集合大小 == 根节点的size
find(int x) : if(p[x] != x) p[x] = find(p[x]); //递归找根节点 (集合的编号)
return p[x];
初始化:p[i] = i; size[i] = 1;
合并:
if(find(a) == find(b)) continue;
size[find(b)] += size[a]; //合并为b所在集合,维护的数量为a+b
p[find(a)] = find(b);
查询:
集合大小 :printf("%d\n",size[find(a)]);
#incldue<bits/stdc++.h>
using namespace std;
int n,m;
const int N = 100010;
int p[N],size[N]; //只保证根节点的size是有意义的就可以!! (记录g根节点代表集合的元素个数)
//如a,b集合合并, 则 ①size[b] += size[a] ,②p[find(a)] = find(b);
int find(int x) //返回祖宗节点 + 路径压缩
{
if(p[x] != x) p[x] = find(p[x]); //递归找根节点 (集合的编号)
return p[x];
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) //依题意初始化
{
p[i] = i; //父节点编号(初始为每个节点自身编号)
size[i] = 1; //初始没有边,每个集合一个元素
//维护(统计)集合数量
}
while(m--)
{
char op[5];
int a,b;
scanf("%s",op);//【c++读入技巧,会忽略末尾空格】
if(op[0] == 'C')
{
scanf("%d%d",&a,&b); //这里要判断一下,若a,b已经在同一个集合里,就continue ,不用做任何操作
if(find(a) == find(b)) continue;
size[find(b)] += size[a]; //合并为b所在集合,维护的数量为a+b
p[find(a)] = find(b); //把一个集合的根节点编号的父节点设置为另一个卷积核的根节点编号
}
else if(op[1] == '1')
{
scanf("%d%d",&a,&b);
if(find(a) == find(b)) printf("Yes");
else puts("No");
}
else //合并集合
{
scanf("%d",&a);
printf("%d\n",size[find(a)]); //集合大小 == 根节点的size
}
}
return 0;
}
堆排序
题目内容
输入一个长度为n的整数数列,从小到大输出前m小的数。 (堆的高度:logn)
输入格式
第一行包含整数n和m。
第二行包含n个整数,表示整数数列。
输出格式
共一行,包含m个整数,表示整数数列中前m小的数。
数据范围
1≤m≤n≤105 , 1≤数列中元素≤109
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
堆的基本应用:
【手写堆基本操作,STL中的堆支持的操作】
1.插入一个数
2.求集合当中的最小值
3.删除最小值
(还可以手写实现删除或修改任意一个元素,STL不能直接实现)
小根堆:父节点都小于等于 子节点的值(小于等于左、右子树)
按照层次遍历存储(完全二叉树的下标 --> 转换成一维存储堆)
性质;x的左儿子 == 2 * x , x的右儿子 == 2 * x + 1;
如何手写一个堆
1.插入一个数 heap[ ++size] = x ;up(size); //往上走,重新变成满足小(或大)根堆 (下标从1开始)
2.求集合当中的最小值 heap[1] ;
3.删除最小值 heap[1] = heap[size] ; size- -; down(1); //①把头和尾交换 ②删去尾部 ③ 让1号节点往下走,重新变成堆
4.删除任意一个元素 head[k] = heap[size]; size- -; down(k); up(k);
5.修改任意一个元素 head[k]; down(k) ; up(k) ;
总结: 用up()和dowm()维护堆的性质,每做一次增删改,都要重新变成堆 为了up和down实际只会执行一个,但为了省代码,不做判断,down和up一起做一遍
简记:
down(int u) 当前点u往下走 min( 父节点,min(左,右)) == 编号(t=u,u*2,u*2+1)
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) //根非最小
{
swap(h[u],h[t]);
down(t);//递归维护堆
}
void up(int u) u往上走 ,父节点下标u/2比较,小根堆(堆顶最小值)
{
while(u / 2 && h[u / 2] > h[u]) 非根时(有父节点(根下标1)不能走为0) && 子节点与父比较
{
swap(h[u / 2],h[u]);
u /= 2;//u节点的编号变成父亲节点编号,再次循环判断直到根节点 u == 1 / 2 --> 0结束
}
}
#include<algorithm>
const int N = 100010;
int n,m;
int h[N],size;
void down(int u) //核心思想: min(根,min(左,右)) == 编号(t=u,u*2,u*2+1)
{
int t = u;//用t存放最小值的编号
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) //根非最小
{
swap(h[u],h[t]);//最小值与节点u交换(若本身最小就不变了)
down(t);//递归维护堆
}
}
void up(int u) //u节点与父节点u/2比较,大的放下面(交换)
{
while(u / 2 && h[u / 2] > h[u]) //非根时(有父节点) && 子节点与父比较
{
swap(h[u / 2],h[u]);
u /= 2;//u节点的编号变成父亲节点编号,再次循环判断直到根节点 u == 1 / 2 --> 0结束
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d",&h[i]);//从1开始存
size = n;
for(int i = n / 2;i;i--) down(i); //可以证明这个循环是O(n)【上界】 ,真实时间看给的层数,带入更精确
while(m--)
{
printf("%d ",h[1]);//输出头部,再把尾部移到头部down(1)维护小根堆, 删除size--头部,
h[1] = h[size];
size--;
down(1);
}
return 0;
}
模拟堆
维护一个集合,初始时集合为空,支持如下几种操作
1."I x"插入一个数 heap[ ++size] = x ;up(size); //往上走,重新变成满足小(或大)根堆
2."PM"求集合当中的最小值 heap[1] ;
3."DM"删除最小值 heap[1] = heap[size] ; size–; down(1); //①把头和尾交换 ②删去尾部 ③ 让1号节点往下走,重新变成堆
4."D k"删除任意一个元素 head[k] = heap[size]; size–; down(k); up(k);
5."C k x"修改任意一个元素
现在要进行N次操作,对于所有的第二个操作,输出当前集合的最小值
第一行输入N
接下来N行输入指令
简记:swap要交换:数值,对应双向指针,h->p;p->h;三个数组互换
模板:down(u),up(u) u为当前节点
main:读入,分类判断,执行对应操作
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int hp[N], ph[N]; //ph[k] = j :存第k个插入点的位置 , hp[j] = k: j值对应第几个插入点 (互为反函数P->h指针存节点编号 和 h->p节点存指针编号)
int h[N]; //堆值
int size;
void heap_swap(int u, int v)
{
swap(ph[hp[u]], ph[hp[v]]); //指针交换
swap(hp[u], hp[v]); //对称指针交换
swap(h[u], h[v]); //值交换
}
void down(int u)
{
int t = u;
if (2*u <= size && h[2*u] < h[t]) t = 2*u;
if (2*u+1 <= size && h[2*u+1] < h[t]) t = 2*u+1;
if (t != u)
{
heap_swap(t, u); //交换要值和指针一起交换
down(t);
}
}
void up(int u) //当前节点往上走
{
while (u/2 && h[u] < h[u/2])
{
heap_swap(u, u/2); //交换要值和指针一起交换
u >>= 1;
}
}
int main()
{
int n;
scanf("%d", &n);
char op[3];
int a, b;
int m = 0;
while (n--)
{
scanf("%s", op);
if (!strcmp(op, "I"))
{
scanf("%d", &a);
m++;
h[++size] = a, ph[m] = size, hp[size] = m;
up(size);
}
else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
else if (!strcmp(op, "DM"))
{
heap_swap(1, size);
size--;
down(1);
}
else if (!strcmp(op, "D"))
{
scanf("%d", &a);
int u = ph[a];
heap_swap(u, size);
size--;
up(u), down(u);
}
else
{
scanf("%d%d", &a, &b);
int u = ph[a];
h[u] = b;
up(u), down(u);
}
}
return 0;
}
week3
143.最大异或对(字典树)
在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N 个整数 A1~AN。
输出格式
输出一个整数表示答案。
数据范围
1≤N≤105
0≤Ai<231
输入样例:
3
1 2 3
输出样例:
3
思路:将每个数以二进制方式存入字典树,找的时候从最高位去找是否有对应位的异或【如1是否有0】
【异或(位运算):两个二进制数对应位不同为1,相同为0】
(字典树:①存储查找字符串集合, ②存储二进制数字,解决位运算)
#include<iostream>
#include<algorithm>
using namespace std;
int const N=100010 , M=31 * N;
int n;
int a[N];
int son[M][2],idx; //M为二进制串长度,分类
void insert(int x)
{
int p=0; //根节点
for(int i=30;i>=0;i--)
{
int u=x>>i&1 ; ///取X的第i位的二进制数 【先向右>>移动i位 ,再&1】
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
}
int search(int x) // 查找x的最大的与或值
{
int p=0;int res=0;
for(int i=30;i>=0;i--) //从最高位开始找
{
int u=x>>i&1;
if(son[p][!u]) //如果当前层有对应的不相同的数
{ //p指针就指到不同数的地址
p=son[p][!u];
res=res*2+1;
//*2相当左移一位 然后找是否有对应位上不同的数res+1
}
else //刚开始找0的时候是一样的所以+0 到了0和1的时候原来0右移一位,判断当前位是同还是异,同+0,异+1
{
p=son[p][u];
res=res*2+0;
}
}
return res;
}
int main(void)
{
cin.tie(0);
cin>>n;
idx=0;
for(int i=0;i<n;i++)
{
cin>>a[i];
insert(a[i]);
}
int res=0;
for(int i=0;i<n;i++)
{
res=max(res,search(a[i]));
}
cout<<res<<endl;
}
240. 食物链(并查集)
动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。
A 吃 B,B 吃 C,C 吃 A。
现有 N 个动物,以 1∼N 编号。
每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y,表示 X 和 Y 是同类。
第二种说法是 2 X Y,表示 X 吃 Y。
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
当前的话与前面的某些真的话冲突,就是假话;
当前的话中 X 或 Y 比 N 大,就是假话;
当前的话表示 X 吃 X,就是假话。
你的任务是根据给定的 N 和 K 句话,输出假话的总数。
输入格式
第一行是两个整数 N 和 K,以一个空格分隔。
以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。
若 D=1,则表示 X 和 Y 是同类。
若 D=2,则表示 X 吃 Y。
输出格式
只有一个整数,表示假话的数目。
数据范围
1≤N≤50000,
0≤K≤100000
输入样例:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例:
3
余1:吃根节点
余2:可以被根节点吃
余0:与根节点同类 (距离为0)
839. 模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
“I x”,插入一个数x;
“PM”,输出当前集合中的最小值;
“DM”,删除当前集合中的最小值(数据保证此时的最小值唯一);
“D k”,删除第k个插入的数;
“C k x”,修改第k个插入的数,将其变为x;
现在要进行N次操作,对于所有第2个操作,输出当前集合的最小值。
输入格式
第一行包含整数N。
接下来N行,每行包含一个操作指令,操作指令为”I x”,”PM”,”DM”,”D k”或”C k x”中的一种。
输出格式
对于每个输出指令“PM”,输出一个结果,表示当前集合中的最小值。
每个结果占一行。
数据范围
1 ≤ N ≤ 105
− 1 0 9 ≤ x ≤ 1 0 9
数据保证合法。
输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6
longlong years later...
文章目录
- 基础算法
- 数据结构
- 数学知识
- 866. 试除法判定质数
- 868. 筛质数 ( (朴素)埃氏筛法、 线性筛法)
- 分解质因数
- 869. 试除法求约数 (试除法)
- 870.约数个数
- 871. 约数之和
- 872. 最大公约数
- 欧拉函数
- 877. 扩展欧几里得算法
- 高斯消元 - 解方程 [暂放 不能写出0/1]
- 885. 求组合数 I C(m,n) 【dp】
- 886 求组合数 II 【数据大小10万级别】 【费马小定理+快速幂+逆元】
- 887. 求组合数 III 【le18级别】 【卢卡斯定理 + 逆元 + 快速幂 】
- 888.求组合数 IV 【没有%p -- 高精度算出准确结果】 【分解质因数 + 高精度乘法 --只用一次高精度提高运行效率】
- 889.满足条件的01序列 【卡特兰数-用法极多!】
- 890.能被整除的数(容斥原理)
- 891.Nim游戏
- 动态规划
hash表 O(1)
【hash里面一般不用sort】
哈希表:存储结构 ①开放寻址法 ②拉链法 (较常用)
字符串哈希方式
若h(x) ∈ [0,105];
①一般取模映射 :x mod 105 **取模为一个质数,且尽可能离2的次方远,【冲突的概率最小】
②解决冲突
一般情况下(99%)实现删除:bool vis[][]变量对应二维坐标,标记false代表删除(没有真的删除)
840. 模拟散列表
维护一个集合,支持如下几种操作:
“I x”,插入一个数x;
“Q x”,询问数x是否在集合中出现过;
现在要进行N次操作,对于每个询问操作输出对应的结果。
输入格式
第一行包含整数N,表示操作数量。
接下来N行,每行包含一个操作指令,操作指令为”I x”,”Q x”中的一种。
输出格式
对于每个询问指令“Q x”,输出一个询问结果,如果x在集合中出现过,则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1 ≤ N ≤ 105
-109≤ x ≤109
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
简记:
const int N = 100003; //取大于n的第一个质数
first_prim(int x) 找到大于n的第一个质数(这个手算也可以)
重点: 因为计算机中%变负数,则把x映射到 0 ~ N 之间的一个数 : (x %N + N) % N
insert(int x) int k = (x % N + N) % N; 邻接表插入,先选头结点
find(int x) int k = (x % N + N ) % N ; for(int i = h[k]; i != -1;i = ne[i])
if(e[i] == x)
return true;
return false;
#include <iostream>
using namespace std;
#include<cstring>
const int N = 100003; //取大于n的第一个质数
int h[N],e[N],ne[N],idx; //一个hash槽h[](存多条链),每个可以拉一条hash链 ,每一条链 e存对应位置元素值 , ne存下一个元素下标 , idx表示当前位置
/*
int first_prim(int x) {//找到大于10^6^的第一个质数为1000003 作为mod值
for(int i = x; ; i++) {
bool flag = true;
for(int j = 2; j <= i / j; j++) {
if(i % j == 0) {
flag = false; //删除标记:false
break;
}
}
if(flag) {
cout << i << endl;
break;
}
}
return 0;
}
*/
void insert(int x) //把x映射到 0 ~ N 之间的一个数 [%N + N]
{ //如x为10^-9^ 就必须先mod再加N
int k = (x % N + N) % N; //【 (如果余数负数 +N ) % N,余数就变成正数】,映射成正数
e[idx] = x; //存值 (这里就想象单链表插入,把idx指向k位置的下一个位置,k的指针指向idx,当前位置idx++)
ne[idx] = h[k]; //idx->next = k->next = k+1 = h[k] (从0开始插入,k+1下标为h[k])
h[k] = idx++; //h[k] = idx(第idx编号次插入) ,idx++
}
bool find(int x)
{
int k = (x % N + N ) % N ;
for(int i = h[k]; i != -1;i = ne[i]) //沿着指针链找下一个点的下标 i = i->next (因为都在一条(初始值为-1)的数组上,最后到NULL即为-1)
if(e[i] == x)
return true;
return false;
}
int main()
{
int n;
scanf("%d",&n);
memset(h,-1,sizeof(h));
while(n--)
{
char op[2];
int x;
scanf("%s%d",op,&x);
//*op 等效op[0]
if(*op == 'I') insert(x);
else
{
if(find(x)) puts("Yes");
else puts("No"); //puts在<iostream>中
}
}
return 0;
}
开放寻址法O(n): (空间开2倍,选质数)
h[x] = k
添加: 找坑位,有人就找下一个坑位
查找: 从第i个坑位开始,如果当前坑位有人,判断是不是x ,如果到坑位没有人,x还未找到,说明x不存在
删除:(查找 + 标记false),而不是真的删除
memset按字节赋值: memset(h,-1,sizeof h) : 则二进制位都是1
简记:find(int x)
while(h[k] != null && h[k] != x)
#include<cstring>
const int N = 200003, null = 0x3f3f3f3f;//每个数没有被使用的数都不在x范围内,作为结束条件
int h[N];
int find(int x)//返回存储x值对应的映射下标k(找不到返回0)
{
int k = (x % N + N) % N ;
while(h[k] != null && h[k] != x) //冲突k++遍历寻找 (为空直接跳出循环,找到x映射下标)
{
k ++;
if(k == N) k = 0;
}
return k;
}
//first_prim(200000);大于此数的第一个质数 2000003 取长度N = 2000003
int main()
{
int n;
scanf("%d",&n);
memset(h,0x3f,sizeof(h)); //按字节赋值,每个字节0x3f ,所有int四个字节就变成0x3f3f3f3f
while(n--)
{
char op[2];
int x;
scanf("%s%d",op,&x);
int k = find(x); //都有此句,写到外面
if(*op == 'I')//*op 等效op[0]
{
//int k = find(x);
h[k] = x; // 先找的x的映射位置k ,再赋值 x
}
else //查找
{
//int k = find(x);
if(h[k] != null) puts("Yes");
else puts("No"); //puts在<iostream>中
}
}
return 0;
}
*字符串前缀hash法 (高效判断字符串-优于kmp)
搜哈希练手
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
简记:
个人认为大写P改成t
ULL get(int l,int r) //计算区间[l,r]hash值
{
return h[r] - h[l - 1] * p[r - l + 1]; 区间[l,r]的hash值公式
}
初始化p[N] , 读入:
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 就可以
}
typedef unsigned long long ULL; // unsigned long long溢出等效取模 [ % 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) //计算区间[l,r]hash值
{
return h[r] - h[l - 1] * p[r - l + 1]; //p^l-r+1^
}
int main()
{
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;
}
STL 常用容器(简介)
vector 变长数组 ,倍增的思想
string 字符串 , substr() , c_str()
queue push() pop() front()
priority_queue 优先队列 push() , top() pop() 【默认是大根堆】
stack 栈 push() top() pop()
deque 双端队列(队头队尾都可以插入删除,且可以随机访问)
set,map,multiset,multimap //基于平衡二叉树(红黑树)实现 ,动态维护有序序列
undordered_set , unorder_map , unordered_multiset , unordered_multimap ,hash表
bitset ,压位
pair<T,T>二元组
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
vector
倍增 (尽量减小申请空间的次数),可以浪费空间【每次到数组长度的时候就把数组空间*2,且复制元素】 变长数组需尽量减少申请次数–用倍增
若申请长度为n = 106 ; 1 + 2 + 4+… + 5*105 = 106
系统为某一程序分配空间时,所需时间与空间大小无关,与申请次数有关
size()
empty()
clear()
front() / back()
push_back() / pop_back()
begin() / end()
[]
【迭代器一定是从0开始】
string 字符串
substr(起始位置,截取长度)
c_str()
size() / length() 都是返回字符串长度
clear()
pair 【自带了一个比较函数,比较first】
first 【sort可以直接引用名称,不用重载】
second
支持比较运算,以first作为第一个关键字,second作为第二个关键字 ,把要比较的放到first位置
queue
size()
empty()
push()
pop()
front()
back()
优先队列 (大根堆)
priority_queue
push()
pop()
top()
priority_queue<int, vector< int >, greater< int > > heap; 【小根堆】
stack
size()
empty()
push()
pop()
top()
deque 双端队列 (功能全, 但缺点:速度慢很多)
size()
empty()
clear()
front() / back()
push_back() / pop_back()
begin() / end() ++ – 返回前驱和后继(前 / 后的一个数) O(logn)
[]
【迭代器一定是从0开始】
set / multiset
insert() 插入
//find() 查找**
count() 返回某一个数的个数
erase()
(1)输入一个数是x,删除所有x O(k + logn)
(2)输入一个迭代器 , 删除这个迭代器
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器,不存在返回end
map / multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 像数组一样用map,时间复杂度是O(logn)
【上述大部分操作是O(logn)】
undordered_set , unorder_map , unordered_multiset ,
unordered_multimap ,hash表
【无序的大部分操作如增删改为O(1),但是不支持排序有关的操作,迭代器的++ --】
无序不支持 lower_bound(x) / upper_bound(x) ,没有顺序
bitset ,压位
比如开一个长度为1024的bool数组,需要1024字节 ,但压到每一位字节存八位 ,就只要 1/8
如bool b[10000][10000]需100M ,但题目限制64M ,
那么压缩后只需 100 * 1/8 ~= 12M
bitset<10000> s
操作:支持位操作
~ , & , | , ^
<<, >>
== , !=
[]
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() , 把所有位置变成1
set(k,v) 将第k位变成v
reset() 将所有位置变成0 (重置)
flip() 等价于~ 取反
flip(k) 把第k位取反
STL使用样例:
#include<bits/stdc++.h> //万能头文件 字节集bits/stdc++.h的头文件
using namespace std;
int main()
{
vector<int> a;
a.size(); //O(1)
a.empty(); //返回数组是否为空
a.clear(); //并不是所有容器都有
for(int i = 0;i < 10;i++) a.push_back(i);
for(int i = 0;i < a.size();i++) cout << a[i] << " ";
cout << endl;
for(vector<int>::iterator i = a.begin() ;i != a.end();i++) cout << *i << " ";
cout << endl;
//for(auto x : a ) cout << x << " "; //5.11不允许 auto ???
cout << endl;
return 0;
}
int main()
{
pair<int , string> p;
p = make_pair(10,"wz");
p = make_pair(20,"abc");
//嵌套 三元组
pair<int, pair<int,string> > pp;
//优先队列(默认大根堆)
priority_queue<int> heap;
int x;cin >> x;
heap.push(-x); //从大到小 【小根堆,输出时输出 (负负得正) -heap.pop() 即可】
cout << heap.top() << endl;
priority_queue<int,vector<int> ,greater<int> > small_heap;
//map的使用
map<string , int> a;
a[];
}
DFS(算法奇特)
不能求最短路 ,数据结构: stack
【一条路走到底,不能走回退】
记录路径上的所有点,存储空间一般O(h)
重点:判断顺序-多种解法
【经典DFS】
①全排列
其他两种全排列方式 【有内置函数】
【从第1个位置开始看,依次填满,每层多个分支(穷举),选取一个递归到下一层,填完一条路,回溯(输出所有种可能需要恢复现场)】
【1-n选择排序填满,排列组合,path从0开始存放】
简记:填的当前位数值从1开始 ,存的路径从0开始【路径输出从0开始】
#include <iostream>
using namespace std;
const int N = 10;
int n;
int path[N]; //临时存路径值 (每一种排列)
bool st[N]; //标记
//【从第0个位置开始看,依次填满,每层多个分支】
void dfs(int u) { //第u位开始填path[u]=i (选取)【如图】
if(u == n) { //填完n位按题目输出(路径path[0]开始存放)
for(int i = 0; i < n; i++) printf("%d ",path[i]); //输出每条临时路径【每种填完的结果】【路径从下标0开始存的】
puts(""); //cout 输出多很慢,就写这个!
//return; //可以不写
}
//填的数值从1开始 ,存的路径从0开始【路径输出从0开始】
for(int i = 1; i <= n; i++) { //【1-n选择排序填满,排列组合】
if(!st[i]) { //找到一个没有用过的数
path[u] = i; //填到当前这个位置上去,并且标记i已经用过
st[i] = true; //每一步要恢复现场 [尝试遍历u=1-n所有可能分支]
dfs(u + 1); // 递归到最深回来结束
st[i] = false;//回退
}
}
}
//修改状态,什么时候进入循环,什么时候标记,什么时候出循环,什么时候恢复
int main() {
cin >> n;
dfs(0);
return 0;
}
STL标准库函数next_permutation : O(n!)
#include <bits/stdc++.h>
using namespace std;
string s;
int main() {
cin >> s;
do {
cout << s << endl;
} while(next_permutation(s.begin(), s.end())); //下一位排序
return 0;
}
②n-皇后
顺序 1:每一行开始看,枚举每一行放到哪个位置上去
顺序2:放 / 不放
1)可以列出所有情况, 再判断是否符合条件
2) 可以边填边判断是否合法,不合法就停止,不继续往下递归
【剪枝 - 提前判断】
对角线 y = x + b 、 y = - x + b --> b = y - x || b = y + x [y = n - u当前]
【截距b的值为对角线的编号】 即左上角和右上角一格为对角线/反对角线的第一条
题目
n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后**都不能处于同一行、同一列或同一斜线上**。
现在给定整数 n,请你输出所有的满足条件的棋子摆法。
输入格式
共一行,包含整数 n。
输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。
其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。
每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格。
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围
1≤n≤9
样例输入
4
样例输出
.Q…
…Q
Q…
…Q.
…Q.
Q…
…Q
.Q…
第一种顺序
【n-皇后写法类似全排列,变循环条件,y = x +b ==> b = y - a * x 和y = -x +b = => b = y - x 】
【则对应对角线为正对角线下标:y+x和反对角线下标: y-x + n(没有负数) 】
【–>对应代码 if(!col[i] && !dg[u + i] && !udg[n - u + i ]) 】
不同棋子每列(每行),对角线,反对角线不能同一条【行列只需列举一个】
path[u] = i; //u列,第i行 --> (i,u)
g[u][i] = 'Q'; //标记已走
#include<bits/stdc++.h>
using namespace std;
const int N = 20; //对角线个数 n*(n-1)
int n;//棋盘大小
int path[N];//存放每轮临时路径【标记用过的】
char g[N][N];//地图
bool col[N] ,dg[N] ,udg[N]; //【列标记 , 对角线--dg,反对角线--udg】
//【反正一条-x+b ,一条x+b
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行遍历【下标0到n-1行】
{
if(!col[i] && !dg[u + i] && !udg[n - u + i]) //列,对角线,反对角线没有放过
{
path[u] = i; //u列,第i行 --> (i,u)
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] = '.';
}
}
}
int main()
{
cin >> n;//n<=3无解
for(int i = 0;i < n;i++)
for(int j = 0;j < n;j++)
g[i][j] = '.';
//dfs(0);
dfs(0);
return 0;
}
第二种顺序 地图每一格子放不放,更接近问题原始 O(n2)
【出口判断较不好记,可理解扩展思路】
【放if(满足限制)标记放了true(x,y+1,s+1) / 不放(遍历一行(x,y+1,s))】
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool row[N],col[N],dg[N],udg[N];
void dfs(int x,int y,int s)//当前判断坐标(x,y) , s统计此轮八皇后已填个数 【类似u(step步数)】
{
if(y == n) y = 0,x ++; //每一行遍历完(再从第一列开始y=0,遍历下一行x++)
if(x == n) //遍历到最后一行
{
if(s == n) //判断此轮s是否有填完n个 【是否成功】
{
for(int i = 0;i < n;i++) puts(g[i]); //打印地图
puts("");
}
return;
}
//不放皇后
dfs(x , y + 1 , s); //判断此行的下一格
//放皇后
if(!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n])
{
g[x][y] = 'Q';
row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
dfs(x , y + 1 , s + 1);//填入1次,s+1
row[x] = col[y] = dg[x + y] = udg[x - y + n] = false; //恢复现场
g[x][y] = '.';
}
}
int main()
{
cin >> n;
for(int i = 0;i < n;i++)
for(int j = 0;j < n;j++)
g[i][j] = '.';
//dfs(0);
dfs(0,0,0);
return 0;
}
BFS(最短最少)
【每次向外扩展一层搜索】
搜索n轮,i = 1 —> n 每轮把每个点到起点的距离为i的点全部搜到
所以能搜最短路径
每层的点都要记录,存储空间O(2h)
数据结构 : queue
【权重都是1时可求最短路】
844. 走迷宫(BFS队列最少步数)
给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。
最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。
数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路。
输入格式
第一行包含两个整数n和m。
接下来n行,每行包含m个整数(0或1),表示完整的二维数组迷宫。
输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围
输入样例:
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例:
8
pair的使用理解:离散的数据存pair不浪费二维数组空间
typedef pair<int,int> PII;
PII t;
记:
【模拟队列 【也可以直接STL库的queue】
【初始化队列 ,出发位置q[0] = {0 , 0} ,取队头(PII t = q[hh ++] )判断】
【用向量表示方向: dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1};】
简记:
bfs():数组模拟队列
初始化,int hh = 0,tt = 0; q[0] = {0 , 0};【下标从(0,0)开始走】
memset(d,-1,sizeof(d)); 第一步:d[0][0] = 0;
【遍历完队列为空hh<=tt】取队头且出队,PII t = q[hh ++]; (可auto)4个方向遍历:
int x = t.first + dx[i], y = t.second + dy[i];
(判边界,障碍)能走的往下走,d[x][y] = d[t.first][t.second] + 1;加步数,q[++tt] = {x,y};【模拟入队先++tt 】
【练习的时候path路径输出可以先不练】
//此题混用pair二元组特性 可以PII q[N*N] , PII prev[N][N], PII t
#include<cstring>
typedef pair<int,int> PII;
//用STL队列: queue<PII> q q.pop() ,q.push
const int N = 110;
int n,m;
int g[N][N];
int d[N][N]; //每一个点到起点的距离
PII q[N * N]; //队列 ,一维二元组q[i] = {x,y} ,[直接转二维数组也行] 【(x,y)=(q[下标].first,q[下标].second) 走过路径】
PII path[N][N]; //【用来记录输出路径 ,二维元组,当做数组】 path[x][y] ,每个路径下标存下一个坐标
/*注意prev有同名的,只能大写P,或不要重叠*/
int bfs() //默认初始位置和终点,不用参数..
{
//初始化准备
int hh = 0,tt = 0; //(队头hh,队尾tt )模拟队列 【也可以直接STL库的queue】
q[0] = {0 , 0}; // 【下标从(0,0)开始走】
memset(d,-1,sizeof d );//初始化-1,代表还没走到
d[0][0] = 0;//顺序不能颠倒 【初始-1,第一个点起点再写为0】
//只要是四个方向就没错 【也可以记每次就dx从-1开始
int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1}; //【用向量表示方向 】
//队列遍历宽搜
while(hh <= tt)//队列不空 【结束条件】
{
PII t = q[hh ++]; //每次先取队头 auto t //这样就模拟出队(每次取完出队)
for(int i = 0;i < 4;i++) //四个方向扩展
{
int x = t.first + dx[i], y = t.second + dy[i]; //(一维二元组PII t和队列q,离散化减少空间)
if(x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) //在边界内 && 不是障碍物 && 未访问(需第一次走才能最短)
{
d[x][y] = d[t.first][t.second] + 1; //当前步数 = 上一步的步数 + 1
path[x][y] = t; //记录每条路径 ,(x,y)这个点是从t这个点走过来的
q[++tt] = {x,y}; //被选取入队(临时队列辅助作用),下一步走的点从队尾进入(开始没有元素)所以也为下一个取的队头
}
}
}
//从后往前导 【输出路径】
int x = n - 1,y = m - 1;
while(x || y) //x != 0 || y != 0
{
cout << x << ' ' << y << endl;
PII t = path[x][y]; //每个路径下标存下一个坐标
x = t.first , y = t.second;
}
return d[n - 1][m - 1]; //右下角点的距离
}
int main()
{
cin >> n >> m;
for(int i = 0;i < n;i++)
for(int j = 0;j < m;j++)
cin >>g[i][j];
cout << bfs() << endl;
}
845.八数码【权重分配】
846. 树的重心
给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数n,表示树的结点数。
接下来n-1行,每行包含两个整数a和b,表示点a和点b之间存在一条边。
输出格式
输出一个整数m,表示重心的所有的子树中最大的子树的结点数目。
数据范围
1≤n≤105
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4
每个点遍历一次dfs或bfs,普通用dfs
思路:
【删除节点x后分出多个连通块】 ,ans = min( 删除节点x ,对应多出的所有连通块中点数最大值 )
dfs可以返回删除节点的子树的点数数量 , 其余所有点(父节点往上) = 总节点数 - 删除点x的子节点总数和
#include<cstring>
#include<algorithm>
const int N = 100010, M = N * 2;
//一条链表一个头head = -1
int h[N],e[M],ne[M],idx; //拉一条链表(N个单链表,N个头) ,e为边的节点编号,ne下一个节点编号 , idx当前点编号
int n,m,ans = N;//初始为题目数值的边界条件 更新用:min(ans,res)
bool st[N];
void add(int a,int b) //插入一条边 【在第a个头结点头插入新结点,值为b】
{
e[idx] = b,ne[idx] = h[a] , h[a] = idx++; //① idx节点赋值 = x ② idx->next = a.head->next ③ a.head->next = idx ④idx++;
}
//以u为根的子树中点的数量
int dfs(int u)
{
st[u] = true;//标记一些,已经被搜过了
int sum = 1 , res = 0; //最少一个节点 res 返回ans
for(int i = h[u];i != -1;i = ne[i]) //链表式遍历!!!
{
int j = e[i];
if( !st[j] )
{
int s = dfs(j); //返回sum
res = max(res , s); //先判断子节点部分的连通块 的最多节点数
sum += s; //计算子节点数总和
}
}
res = max(res , n - sum);//再计算 剩余节点个数 ,比较大小
ans = min(ans , res);
return sum;
}
int main()
{
cin >> n ;
memset(h,-1,sizeof h);
for(int i = 0;i < n - 1;i++)
{
int a,b;
cin >> a >> b;
add(a,b),add(b,a); //无向图 (树)
}
dfs(1); //可以选 1 --- n 之间任意点 ,答案一样!
cout << ans << endl;
return 0;
}
树和图的存储
树是一种特殊的图(无环联通图)
图:有向图,无向图 ,邻接表,邻接矩阵
【数组模拟邻接表】
【邻接表 == 多个单链表,即多个头结点 h[N] ,e[N],ne[N],idx 】
邻接表深搜框架
#include<cstring>
#include<algorithm>
const int N = 100010, M = N * 2;
//一条链表一个头head = -1
int h[N],e[M],ne[M],idx; //拉一条链表(N个单链表,N个头) ,e为边的节点编号,ne下一个节点编号 , idx当前点编号
int n,m,ans = N;//初始为题目数值的边界条件 更新用:min(ans,res)
bool st[N];
void add(int a,int b) //插入一条边 【在第a个头结点头插入新结点】
{
e[idx] = b,ne[idx] = h[a] , h[a] = idx++; //① idx节点赋值 = x ② idx->next = a.head->next ③ a.head->next = idx ④idx++;
}
void dfs(int u)
{
st[u] = true;
for(int i = h[u];i != -1;i = ne[i])
{
int j = e[i];
if(!st[i])dfs(j);
}
}
int main()
{
memset(h,-1,sizeof h);
dfs(1);
}
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】
直接用队列< queue >
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);
}
}
}
【邻接表+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[] 存终点指向的边,ne存起点边下标,h[]头结点指向每个链表第一个节点下标
{
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);
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)//j没有走过
{
d[j] = d[t] + 1; //若j没被扩展d[j]为-1,就j扩展这个点
q[++tt] = j; //入队
}
}
}
return d[n];
}
int main()
{
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,不会入队(入队的次数队尾tt统计,tt == n-1成立说明无环)
所有的边都是从前指向后的, 入度为0的点可以作为起点(没有任何一条边指向此点,没有前驱)
d[i] 存储点i的入度
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]; // d[i] 存储点i的入度
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 main()
{
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] ++ ; //a指向b的边 ,b点下标的入度++
}
if(topsort())
{
for(int i = 0;i < n;i++) printf("%d ",q[i]); //发现队列中的元素顺序就是topo排序顺序
puts("");
}
else puts("-1");
return 0;
}
【调代码 ①中间变量 ②删代码(注释) ,判断哪部分出错】
学习重点:代码 + 思路
【正权边:Dijkstra比较好 , 负权边:SPFA比较好 ,限制k步走到用Bellman-Ford,多源:Floyd 】
(无向图为特殊有向图:a->b && b->a ,所以算法用有向图写法)
本章导图 【此处注意每种算法怎么输出路线,最短距离等】
①单源最短路
所有边权都是正数
朴素Dijkstra算法 O(n2) ,n为点数 ,m为边数 【稠密图使用】
堆优化版的Dijkstra算法 O(mlogn) 【稀疏图使用】
存在负权边
Bellman-Ford O(nm) [不超过多少条边,此算法做]
SPFA 一般:O(m) ,最坏 O(nm) 【目前掌握这个就可以】
②多源汇最短路
Floyd算法 O(n3) 【dp】
最短路【正权边】
849. Dijkstra求最短路 I 【朴素Dijkstra算法 O(n2)】
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示点x和点y之间存在一条有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
思考旧:
dist[1] = 0; memset(dist,0x3f,sizeof dist); for迭代 初始 t = -1(表示还没有确定);
st[j] = true当前已经确定最短路的点
找t不在st中的,距离最近的点,加入st ,t = j ,循环下一步
用1 – t路径的长度 + t到其他点 j 的距离 ,更新最短路径长度
【dist点下标从1开始,初始化dist[1]=0,循环 j = 1 ~ n】
(给定起点终点的一条最短路)
【路径距离判断dist[j] = min(dist[j] , dist[t] + g[t][j]); st[N]加入确定的最短路的点 】
简记:
main:初始化邻接矩阵(题目有重边选最小)
dijkstra :①初始化dist 按字节0x3f ,dist[1]=0【dist从1号点开始】,n轮迭代遍历n个点,每轮选取第j点,i :0->n , 正无穷:【0x3f3f3f3f】
②第j点更新所有路径min(dist[j] , dist[t] + g[t][j]); min(起点 --> j , 起点 -->t(新加的第j点,更新所有路径) --> j
j : 1-> n
#include <iostream>
using namespace std;
#include<algorithm>
#include<cstring>
const int N = 510;
int n,m;
int g[N][N]; //g[x][y] 存权值val ,边 x->y 权val
int dist[N];//距离 , 初始化INF无穷大,每个字节3f(16进制)0x3f
bool st[N]; //点下标1开始
int dijkstra()
{
memset(dist,0x3f,sizeof dist); //距离初始化正无穷【0x3f3f3f3f】
dist[1] = 0;//起点位置0 【dist从1号点开始】
for(int i = 0;i < n;i++)//找n次最小,迭代n轮(所有点均需尝试访问,则需n轮)
{ //当前下标t ,初始-1
int t = -1; //出发,选取一个点j, 每轮找到一个经过第j点更短路径,加入第j点,更新所有路径
for(int j = 1;j <= n; j++) //遍历所有点,找最短路
if(!st[j] && (t == -1 || dist[t] > dist[j]) ) //,未访问 && ( t==-1(未开始)或(找到更短的)dist[t]不是最短的)
t = j;//不在st中且距离最短的点
//【j加入st,t = j标记st[t] = true】
//位置不理解 if(t == n)break;//已经遍历所有点,优化剪枝结束
st[t] = true; //标记访问,把t = j加入集合
//用1 -- t路径的长度 + t到其他点的距离 ,更新路径长度
for(int j = 1; j <= n ; j++)
dist[j] = min(dist[j] , dist[t] + g[t][j]);
//min(起点 --> j , 起点 -->t(用新加的点,更新所有路径)+ t-->j)
}
//按题目,判断第n号点min
if(dist[n] == 0x3f3f3f3f) return -1; //【未走到n,仍为初始值INF】不可达
return dist[n];
}
int main()
{
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g); //邻接表初始化
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = min(g[a][b],c); //题目存在重边,自环,选取最小边(带入算法)判断最短路
}
int d = dijkstra(); //dist_max
cout << d <<endl;
return 0;
}
Dijkstra求最短路 II(堆优化-优先队列+邻接表)
题意:给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n,m≤1.5×105,
图中涉及边长均不小于0,且不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
思路:【类似BFS】
初始化
【优先队列维护距离,链表遍历,取队头判断distance = t.first(权重) ,ver = t.second(下标)】
STL优先队列(堆, 缺点:冗余存在),priority_queue<PII,vector< PII >,greater< PII > > heap PII {距离,下标}
邻接表 h[N],e[N],ne[N],idx; 加权重w[N] (重边不用处理) 稀疏图
简记:
main:初始化邻接表,add(x,y,value) ,链表插入
dijkstra : 小根堆(根最小):**priority_queue<PII,vector< PII >,greater< PII > > heap**
auto t = heap.top() ;heap.pop()
dist[1] = 0 -->放入堆 heap.push {0,1}
int distance = t.first,ver = t.second ; //PII {距离,下标}
if(st[ver] ) continue; //已经判断过了,跳过
for(int i = h[ver]; i != -1 ;i = ne[i])
遍历放入最小值heap.push({dist[j],j}); heap.push {距离,下标}
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue> //堆 - 邻接表
const int N = 510;
typedef pair<int,int > PII;
int n,m;
int h[N],w[N],e[N],ne[N],idx; //邻接表【多条单链表】 , 多了个权值w[N]
int dist[N];
bool st[N];
void add(int a,int b,int c)//每个头结点存放每条链表第一个元素下标(第a边下标,h[a])
{
e[idx] = b, w[idx] = c , ne[idx] = h[a] ,h[a] = idx++ ; //a,b编号 ,c权值
}
int dijkstra()
{
memset(dist,0x3f,sizeof(dist)); //距离初始化正无穷
dist[1] = 0;//起点距离自己0
priority_queue< PII , vector<PII> , greater<PII> > heap; //优先队列的参数定义 !【小根堆,根为最小值】
heap.push({0,1}); //放入起点
while(heap.size()) //迭代n轮 队列为空,size=0停止
{
PII t = heap.top(); //auto t
heap.pop(); //取队头,出队【辅助作用,每个队头为当前走到的点】
int distance = t.first,ver = t.second ; //PII {距离,下标}
if(st[ver] ) continue; //已经判断过了,跳过
//链表遍历判断更新
for(int i = h[ver]; i != -1 ;i = ne[i]) //链表遍历下标 ,点下标ver = t.second
{
int j = e[i];
if(dist[j] > distance + w[i]) //找最短路
{
dist[j] = distance + w[i];
heap.push({dist[j],j}); //把j点放入确定最短路集合
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1; //不可达
return dist[n];
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c); //邻接表重边没事
}
int t = dijkstra();
cout << t <<endl;
return 0;
}
模板级补全堆优化版:
int dijkstra() // 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1});
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[ver] + w[i])
{
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
最短路 【负权边】
853. 有边数限制的最短路(Bellman-ford)
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出 impossible。
数据范围
1≤n,k≤500
1≤m≤10000
任意边长的绝对值不超过 10000。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
思路:
n次循环,每次循环所有边 O(n2)
struct edge{
a, b, w;
}edges[N];
dist[b] = min(dist[b] , dist[a] + w); //松弛操作
所有边满足 dist[b] <= dist[a] + w; //三角不等式
负循环路:这条路权和 < 0 ,死循环到负无穷 …无解
【可特别使用场景:经过不超过k条边到终点的最短路的距离】
【可求负环,只有负环在1 – n 的路径中存在,才无解(无最短路)】
n条边 ,n+1个顶点,【依据抽屉原理,若小于n+1个顶点,必有环】
简记:
main:初始化结构体数组 edges[i] = {a,b,w}; //{顺序赋值} C++11 新特性,赋值
贝尔曼只要能遍历结构体数组edges[N]
for n次 (k次,则限制为经过k条边的最短路径)
for 所有边 a,b,w
dist[b] = min(dist[b], backup[a] + w);
memcpy(backup,dist,sizeof(dist)); 复制备份
dist[b] = min(dist[b], backup[a] + w);
if(dist[n] > 0x3f3f3f3f / 2) return -1; 有负权边
#include<cstring>
#include<algorithm>
const int N = 510,M = 10010; // 经过边数k, 边数最大值M
int n,m,k;
int dist[N] , backup[N]; //备份
struct Edge
{
int a , b ,w;
}edges[M];
int bellman_ford()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
for(int i = 0;i < k;i++) //经过不超过k条边,到达各个点的最短距离
{
memcpy(backup,dist,sizeof(dist));
//每次迭代前,先备份,因为k的限制,不一定是最短路为答案【只用上一次迭代结果 【否则可能发生串联,用了非最短的再去更新其他】
for(int j = 0;j < m;j++)
{
int a = edges[j].a , b = edges[j].b ,w = edges[j].w;
dist[b] = min(dist[b], backup[a] + w);
}
}
if(dist[n] > 0x3f3f3f3f / 2) return -1;// 0x3f3f3f3f / 2 因为负权边
return dist[n];
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i = 0;i < m;i++)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i] = {a,b,w}; //{顺序赋值} C++11 新特性,赋值
}
int t = bellman_ford();
if(t == -1) puts("impossible");
else cout << t << endl;
return 0;
}
851.spfa求最短路 【BFS】
给定一个 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
思路:
把起点放入队列
while(!q.empty())
只要队列里面还有节点变小,就更新
①t = q.front()
q.pop()
② 更新t的所有出边,
找最短min(dist[b] == w) t = b q.push(b)
有负环用SPFA,部分正权图不卡范围也可以SPFA(通用)
简记: 类似堆优化dijkstra
main: 初始化邻接表(照搬堆优化dijkstra)
spfa:while(q.size()) //队列不空
for(int i = h[t] ; i != -1; i = ne[i]) 链表遍历
j=e[i] ; 更新到第j边的最短路
queue<int> q;
int t = q.front(); // 取队头 front
q.pop();
#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) {
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; //未确定最短路
for(int i = h[t] ; i != -1; i = ne[i]) { //链表遍历 队头t的邻接边,
int j = e[i]; //j代表判断当前边编号e[i] ,更新到第j边的最短路
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 main() {
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--) {
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c); //邻接表重边没事
}
int t = spfa();
if(t == -1) puts("impossible");
else printf("%d\n",t);
return 0;
}
852. spfa判断负环(SPFA算法)
题目描述 :
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入输出格式 :
输入
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出
如果图中存在负权回路,则输出 Yes,否则输出 No。
输入输出样例 :
输入
3 3
1 2 -1
2 3 4
3 1 -4
输出
Yes
SPFA负环判断思路:
int cnt[N] //存边数
设有n个点
抽屉原理:经过n条边,对应n+1个点,
所以一定有一个重复遍历,这就说明图中存在负环
至少有两个节点编号相同
只要过程中cnt[x] > n;//有负环
引用~
用SPFA算法判断负环,仅需在原有算法的基础上,维护一个cnt[N]数组,记录某个节点到源点最短路所需的边数,
然后根据抽屉原理,如果cnt[i] >= n ,即从原点到i点所经过的边数大于等于n条,又因为n条边对应n + 1个点,
但因为整个图只有n个点,所以一定有一个重复遍历,这就说明图中存在负环,只有通过负环使最短路径减小,才会遍历到相同的点。
因此,在每一次松弛成功时,令cnt[j] = cnt[t] + 1,
这是由松弛的定义得到的,以t为中转站,经过源点经过t所到达j的点是否比原来的更小,
如果更小,则要走经过t的路径,即在cnt[t]的基础上加1,同时还要判断是否cnt[j] >= n,
如果成立,则直接返回true。在初始化队列时,我们也要做出相应的改变,由于负环从源点1不一定能够到达,
我们要将图中所有的点都压入队列中,作为初始点集,这样便可以考虑并处理所有情况。
//对SPFA稍加改进
#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; //多了个权值
int dist[N],cnt[N];
bool st[N];
void add(int a,int b,int c) {
e[idx] = b, w[idx] = c , ne[idx] = h[a] ,h[a] = idx++ ; //a,b编号 ,c权值
}
int spfa() {
//求的是图是否存在负环 , 不需要初始化 ,存在负环就返回true
//把所有点全部放在st[i]数组true ,全部入队q.push(i)
queue<int> q;
for(int i = 1;i <= n;i++)
{
st[i] = true;
q.push(i);
}
while(q.size()) { //队列不空
int t = q.front();
q.pop();
st[t] = false; //未确定最短路
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];
cnt[j] = cnt[t] + 1;//补充步骤 == j的边的数量等于前面的t + j 自己这条路-- >边数达到n说明有负环 -- 抽屉原理
if(cnt[j] >= n) return true; //超过n条,存在负环
if(!st[j]) { //j未确定最短路
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--) {
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c); //邻接表重边没事
}
if(spfa())puts("Yes");
else puts("No");
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
邻接矩阵存图 ,存所有的边
算法结束后,d[a][b]表示a到b的最短距离
三重循环 i , k , j – > n
#include< algorithm> 中的min要写!
状态转移方程:d[i,j] = min(d[i,j] , d[i,k] + d[k,j] )
d[k,i,j] 三维: 从i出发经过k这个点的最短距离
d[k,i ,j] = d[k - 1,i,k] + d[k - 1,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 main()
{
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); //重边(多条a->b)保留最小权值
}
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;
}
最小生成树
①prim普利姆算法
朴素版 O(n^2^)(代码简单) 【稀疏图】
堆优化版 O(mlogn)(不常用)
②Kruskal克鲁斯卡尔 O(mlogm) 【稠密图】
二分图
染色法O(n + m)
匈牙利算法 O(mn) 实际运行时间远小于O(mn)
prim普利姆算法 【朴素版】
给定一个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。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边的边权的绝对值均不超过10000。
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
思路旧:
dist[i] 初始正无穷 [连通路径集合]
for(int i = 0 ;i < n;i++)
{
t = 找到集合外距离最最近的点 , 集合指已经确定路径的点(已经加入连通块)
用t更新它到集合的距离
st[t] = true; //标记此点已访问
}
可与迪杰斯特拉算法对比【不能说很像,只能说一模一样emmm~】 【思路,遍历,初始 ~类似】
【把思路翻译成时间复杂度】
简记:
main: 邻接表初始化,取重边最小权
prim: n次加点
类似Dijkstra判断最小权,j:1-->n
if(i && dist[t] == INF) return INF; //不可达 ,图是不连通的,不存在最小生成树
if(i) res += dist[t]; // 先累加再更新,防止负环
选取完,更新最小边权 ,j:1-->n
加入集合 st[t] = true;
#include <iostream>
using namespace std;
#include<algorithm>
#include<cstring>
const int N = 510,INF = 0x3f3f3f3f;
int n,m;
int g[N][N];
int dist[N];
int st[N];
int prim()
{
memset(dist,0x3f,sizeof dist);
int res = 0; //返回最小长度之和
for(int i = 0;i < n;i++)
{
int t = -1;
for(int j = 1;j <= n;j++)
if(!st[j] && (t == -1 || dist[t] > dist[j])) // (还没有找到任何一个点||当前距离大于起点到j的距离)
t = j;
if(i && dist[t] == INF) return INF; //不可达 ,图是不连通的,不存在最小生成树
if(i) res += dist[t]; // 先累加再更新,防止负环
for(int j = 1 ; j <= n ; j++) dist[j] = min(dist[j] , g[t][j]); //【选取第j边的最短距(非路径整体最短距)】
st[t] = true;
}
return res;
}
int main()
{
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g);
while(m --)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = g[b][a] = min(g[a][b] , c); //树是无环连通无向图
}
int t = prim();
if(t == INF) puts("impossible");
else printf("%d\n",t);
return 0;
}
859. 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 权重w
if a,b不连通 ,将这条边加入集合
【数据结构837.连通块操作 - 并查集的简单应用】 O(m)
简记:【重载 '<' +sort + 并查集p 】
edges[N] 结构体数组: a,b,w ,重载 '<' ,w从小到大排序
find 函数: 并查集
main : 初始化输入
sort(edges,edges+m)排序m条边,取前n个即最小边权构造生成树
for(int i = 1; i < m; i++) p[i] = i; //并查集初始化 ,边下标 i: 1 --> m
并查集合并:循环并成一个集合
按题目输出 cnt < n - 1 : impossible ; else : res
bool operator < (const Edge &W)const { //重载' < '按权重排序 ,【记参数引用 + 2*const】
return w < W.w;
}
res 最小生成树边权和
cnt 判是否无环(能构成生成树)
/*
用c++11新特性赋值 edges[i] = {a,b,w};
Edge(){}
Edge(int _a ,int _b , int _w) {
a = _a;
b = _b;
w = _w;
}
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n,m;
int p[N]; //并查集
struct Edge
{
int a,b,w;
bool operator < (const Edge &W)const { //重载' < '按权重排序 ,【记参数引用 + 2*const】
return w < W.w;
}
} edges[N];
int find(int x) { //并查集
if(p[x] != x ) p[x] = find(p[x]);
return p[x];
}
int main() {
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,w};
//edges[i] = Edge(a,b,w);
}
sort(edges,edges + m); //重载小于号,按权w从小到大排序
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) { //合并集合操作 【所有点都加入集合,刚好n个点,n-1条边】
p[a] = b;
res += w;
cnt ++ ;
}
}
if(cnt < n - 1) puts("impossible");
else printf("%d\n",res);
return 0;
}
二分图
二分图 <==> 当且仅当图中不含有奇数环 [顺时针编号:1 2 1 2 1 2 1 2 1 2]
证明:充分性 --> 必要性(能反推) <–
遍历点,二分编号1 ,2 所有1连通,所有2连通 , 分开染色
由于没有存在奇数环,所有染色过程当中一定是没有矛盾的
860. 染色法判定二分图O(n+m)
给定一个n个点m条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图。
输入格式
第一行包含两个整数n和m。
接下来m行,每行包含两个整数u和v,表示点u和点v之间存在一条边。
输出格式
如果给定图是二分图,则输出“Yes”,否则输出“No”。
数据范围
1≤n,m≤105
输入样例:
4 4
1 3
1 4
2 3
2 4
输出样例:
Yes
思路:判断是不是二分图 (在染色中出现过矛盾,不是奇数环 ,不是二分图 ;反之能完整染色一遍就是二分图)
for(int i = 1;i <= n;i++)
if(i未染色)
dfs(i,1) //染成1号颜色
简记:
main: 邻接表初始化
dfs(u,c):
链表循环
dfs(j,3 - c) 【1+2 = 3,互为减数 , 1 2 1 2 1 2 循环 】
#include<cstdio>
#include<cstring>
const int N = 200010;
int n,m;
int h[N],e[N],ne[N],idx;
int color[N];
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a] , h[a] = idx++;
}
bool dfs(int u,int c) //当前的节点编号 , 颜色编号
{
color[u] = c;//当前节点的颜色是c
for(int i = h[u];i != -1;i = ne[i])
{
int j = e[i];
if(!color[j])
{ //如果dfs成功就返回 true ,!dfs就不会执行,否则就失败返回false
if(!dfs(j,3 - c)) return false; //c已用,改用3-c染成另一种颜色 【不断循环 1 2 1 2】
}
else if(color[j] == c) return false; //一条边的两边不能是一样的颜色 ,是就有矛盾,return false
}
return true; //全都染色跳出循环
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b) , add(b,a);
}
bool flag = true;
for(int i = 1;i <= n;i++)
if(!color[i])//若未染色
{
if(!dfs(i,1)) // 定义若dfs : false
{
flag = false; //没有矛盾发生
break;
}
}
if(flag)puts("Yes");
else puts("No");
return 0;
}
861.二分图最大匹配(匈牙利算法)
想提升372.棋盘覆盖
题目描述
给定一个二分图,其中左半部包含 n1 个点(编号 1~n1),右半部包含 n2 个点(编号 1~n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数
输入格式
第一行包含三个整数 n1、 n2 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。
输出格式
输出一个整数,表示二分图的最大匹配数。
数据范围
1≤n1,n2≤500,
1≤u≤n1 ,
1≤v≤n2,
1≤m≤105
输入样例
2 2 4
1 1
1 2
2 1
2 2
输出样例
2
思路:
给定一个二分图 ,求匹配数量最多的匹配
月老:男女牵线(不脚踏两只船) ,最多可以牵多少条线 男看上的姑娘【可选连接匹配】
若两个男生喜欢同一个姑娘,这个时候冲突,就先看姑娘喜欢那个男生,若有【没有找到匹配的男生,有已经女生的喜欢,则此女生可以换链】
只有全部的链都不满足男的需求,【或已经相互选择】,才放弃
【最后悔的是错过一件事,而不是做错一件事】
扩展难题:372.棋盘覆盖
算法描述:
如果你想找的妹子已经有了男朋友,
你就去问问她男朋友,
你有没有备胎,
把这个让给我好吧
多么真实而实用的算法
简记:
main:邻接表 add
find 匈牙利算法: 第x头结点的链表遍历
if(match[j] == 0 || find(match[j])) //如果这个妹子还没有匹配任何男生 || 虽然匹配这个男生,但是这个男生可以找的下家(有多个匹配match[j]指向的男生),妹子就可以选择空出来,配对当前男生
{
match[j] = x;
return true;
}
#include<cstring>
#include<algorithm>
#include<bits/stdc++.h>
const int N = 510, M = 100010; //虽然是无向图,但是只会找左边指向右边 【my_选择性映射】,存一边即可 M只需100010
int n1,n2,m;
int h[N],e[M],ne[M],idx; //数组越界会发生各种错误 , 如TLE 超时
int match[N];//为匹配初始0【不用初始了】 右边的点对应的(女对应的男)
bool st[N];//标记遍历确定【点是否使用】状态,初始false ,每轮判断用,每轮重新版变false
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a] , h[a] = idx++;
}
bool find(int x)//输入男生编号
{
for(int i = h[x];i != -1;i = ne[i])
{
int j = e[i];
if(!st[j]) //还没有确定匹配
{
st[j] = true;
if(match[j] == 0 || find(match[j])) //如果这个妹子还没有匹配任何男生 || 虽然匹配这个男生,但是这个男生可以找的下家(有多个匹配match[j]指向的男生),妹子就可以选择空出来,配对当前男生
{
match[j] = x; //女生j配对男生x
return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d%d",&n1,&n2,&m);
memset(h,-1,sizeof h);//初始
while(m --)//录入数据 , 只要单边连,即为匹配,add一个方向即可
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
}
int res = 0;
for(int i = 1;i <= n1;i++) //遍历男生
{
memset(st,false,sizeof st); //每轮判断状态回溯,match为总体
if(find(i)) res ++;
}
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< string > 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 main()
{
string start;
for(int i = 0;i < 9;i++) //字符串读入
{
char c;
cin >> c;
start += c;
}
cout << bfs(start) << endl;
return 0;
}
数学知识
1.数论 【每一步都要想时间复杂度,看能不能做】
2.组合计数
3.高斯消元
4.简单博弈论
866. 试除法判定质数
给定 n 个正整数 ai,判定每个数是否是质数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。
输出格式
共 n 行,其中第 i 行输出第 i 个正整数 ai 是否为质数,是则输出 Yes,否则输出 No。
数据范围
1≤n≤100,
1≤ai≤231-1
输入样例:
2
2
6
输出样例:
Yes
No
质数定义:在大于1的整数中,如果只包含1和本身这两个约数,就被看成质数 , 反之为合数
(1)质数的判定—试除法 O(n1/2) - - - O(sqrt(n))
质数性质:;[d * n / d == n] --> d | n 【'|'表示整除】, 则 n/d | n 枚举成对出现的小的部分的数,即可推全部: d <= n/d – > d2 <= n 【优化原理】
(2)分解质因数—试除法 O(n1/2)
优化:n中最多只包含一个大于 n1/2 ,【证明:质因子相乘不会大于n,反证出最多一个】
筛去质数的倍数
埃及的一个数学家发明的:埃氏筛法
一个质数定理: 1 ~ n中有 n / logn 个质数 n(1 + 1/2 + 1/4 + 1/8 + 1/16 …)调和级数 O(nloglogn) ~= O(n)
线性筛法
- i % pj【primes[j] 】== 0 ,primes[j] 一定是i的最小质因子
2.i % pj != 0 , pj一定小于i的所有质因子,pj也一定是 pj * i 的最小质因子
对于一个合数x,假设pj是x的最小质因子,当i枚举到x / pj的时候
每个数只被筛选过一次
868. 筛质数 ( (朴素)埃氏筛法、 线性筛法)
【最佳选用线性筛法】
1、题目:
给定一个正整数n,请你求出1~n中质数的个数。
输入格式
共一行,包含整数n。
输出格式
共一行,包含一个整数,表示1~n中质数的个数。
数据范围
1≤n≤106
输入样例:
8
输出样例:
4
2、引用~基本思想:
(1)朴素筛法:不管是合数还是质数,筛掉2~n中每个数的倍数 O(nlogn);
(2)埃氏筛法:因为每个合数都是由有限个质数相乘得到的,仅筛掉质数的倍数 O(nloglogn);
(3)用线性筛法快(建立质数表):在埃式筛法中,有的合数会被不同的质数筛多次,会浪费时间,
而每个合数的最小质数是唯一的(否则就不是最小质数),所以只需要用每个合数的最小的质数将它筛掉一次 即可O(n)。
判断素数
#include <iostream>
using namespace std;
bool is_prime(int n)
{
if(n < 2)return false; //先排除非法值
for(int i= 2;i <= n / i ; i++) //[防止溢出写法]
if(n % i == 0)
return false;
return true; //如果false会直接返回,所以可以不加else
}
埃式筛法 (朴素)
#include<bits/stdc++.h>
using namespace std;
const int N = 10000010;
int primes[N] ,cnt;
bool st[N];
bool get_prims(int n)
{
for(int i = 0;i <= n;i++)
{
if(!st[i]) //st质数表
{
primes[cnt ++] = i;
for(int j = i + i;j <= n;j += i) st[j] = true;
}
}
}
线性筛法
同样n只会被最小质因子筛掉 【提速原理:不会重复筛选质数的倍数】
简记:每找到一个质数 , 筛质数表中存放的质数,必筛到 i 的最小质因子
st ==true标记合数,每次j遍历已找到的质数(到0未赋值停止)
st[primes[j] * i] = true; [已有质数*质数i] ,筛出合数标记为 : true
if(i % primes[j] == 0)break; 必为最小质因子
#include<bits/stdc++.h>
using namespace std;
const int N = 10000010;
int primes[N] ,cnt;
bool st[N];//true筛去 ,剩下未被标记的为质数
void get_prims(int n) //【找质数-不是判断质数,建立质数表】
{
for(int i = 2;i <= n;i++)
{ //因为i从2开始找质数 2,3,5都是质数,且4是2的倍数,在2判合数时已经被标记,会跳过
if(!st[i]) primes[cnt++] = i;//primes质数表cnt统计质数个数
for(int j = 0;primes[j] <= n / i;j ++) //遍历到 n/(自身= i) 即可
{
st[primes[j] * i] = true; //st为true筛去合数 ,剩下false为质数 [已有质数*i倍,标记合数]
if(i % primes[j] == 0)break;//成立时 , primes[j] 一定是i的最小质因子 (不成立时,primes[j]一定小于j的最小质因子,则也包含i的最小质因子)
}
}
}
int main() //输出n以内的所有质数(包括n)
{
int n;
cin >> n;
get_prims(n);
for(int i = 0;primes[i]; i ++) printf("%d ",primes[i]);
puts("");
return 0;
}
分解质因数
复杂度介于 O(logn) ~ O(n1/2)
简记:质因数^cnt次方 的乘积
联系约数个数:就是unorder_map<int,int > primes 存放[ 质因数,个数(幂)]
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("");
}
869. 试除法求约数 (试除法)
题目描述 :
给定 n 个正整数 ai,对于每个整数 ai,请你按照从小到大的顺序输出它的所有约数。
输入输出格式 :
输入
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出
输出共 n 行,其中第 i 行输出第 i 个整数 ai 的所有约数。
输入输出样例 :
输入
2
6
8
输出
1 2 3 6
1 2 4 8
思路:
①枚举 1 ~ sqrt(n) ,判断是否x % i == 0
②因为约数都是成对出现【 d * ( n/d ) == n --> d | n ,n/d | n [‘|’:整除],只枚举到小的那个约数就可以】
若是x % i != i,我们再将x % i也储存到结果数组
③对res进行sort排序algorithm
简记: 开vector< int > res
res.push_back(i) : 约数存放 i与 n/i ( i != n / i)不要重复存放
sort(res.begin(),res.end());
main:for(auto t : res) printf("%d ",res[t]) ; puts(""); 【c++11】 迭代器遍历换成for 新型
或 for(int t = 0;t < res.size();t++) printf("%d ",res[t]) ; puts("");
#include<bits/stdc++.h>
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
vector<int> get_divisors(int n)//试除法
{
vector<int> res;
for(int i = 1;i <= n / i ;i++) //枚举到sqrt(n) ,这样写是为了体现成对出现的约数 可以改成i * i <= n
if(n % i == 0) //是约数 ,同时 n/i 也是约数
{
res.push_back(i); //放入
if(i != n / i) res.push_back(n / i); // 平方相等时,i 与 n/i 两个约数一样,不要重复放
}
sort(res.begin(),res.end());
return res; //数组首地址
}
int main() {
int n;
cin >> n;
while(n --)
{
int x;
cin >> x;
vector<int> res = get_divisors(x);
for(auto t : res)printf("%d " , t );
puts("");
/*法二 :【vector数组下标循环(非迭代器)】
for(int t = 0;t < res.size();t++)
{
cout << res[t] << " ";
}
puts("");
*/
}
return 0;
}
870.约数个数
给定n个正整数ai,请你输出这些数的乘积的约数个数,答案对109+7取模。
输入格式
第一行包含整数n。
接下来n行,每行包含一个整数ai。
输出格式
输出一个整数,表示所给正整数的乘积的约数个数,答案需对109+7取模。
数据范围
1≤n≤100,
1≤ai≤2*109
3
2
6
8
输出样例:
12
解题思路:约数个数等于各个质因子的幂指数+1 的乘积,然后利用这个定理,先把各个数分解质因子,用hash表存储幂指数,最后乘起来即可。
分解定理:每种数的分解选法对应一种约数个数
1 ~ n的约数个数共有nlogn , 平均每个数logn个
(所有指数+1 )再相乘 = 约数个数
简记:
联系约数个数:就是unorder_map<int,int > primes 对应关系 [ 质因数下标key, 个数(幂)]
【unorder_map 存储 {key, mapped} 对的哈希表 】
#include<bits/stdc++.h>
using namespace std;
#include<algorithm>
#include<unordered_map>
const int mod = 1e9 + 7;
typedef long long ll;
int main()
{
int n;
cin >> n;
unordered_map <int,int> primes;
while(n --)
{
int x;
cin >> x; //累加每个a[i]的质因子个数
for(int i = 2;i <= x / i;i++)
while(x % i == 0)
{
x /= i;
primes[i] ++;//存了所以质因数的指数
}
if(x > 1) primes[x] ++;
}
ll res = 1; //乘积初始值: 1
for(auto prime : primes) res = res * (prime.second + 1) % mod; //仅此不同
cout << res << endl;
return 0;
}
871. 约数之和
给定 n 个正整数 ai,请你输出这些数的乘积的约数之和,答案对 109+7 取模。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出一个整数,表示所给正整数的乘积的约数之和,答案需对 109+7 取模。
数据范围
1≤n≤100,
1≤ai≤2×109
输入样例:
3
2
6
8
输出样例:
252
用算术基本定理分解质因数:
unordered_map<int, int>一个用来存P,一个用来存对应的α;
记住约数之和公式:( P10 + P11 + … + P1α1 ) × … × ( Pk0 + Pk1 + … + Pk^α k^ ) 【证明:展开式相等】
乘积的个数 (α1 + 1)(α2 + 2)* … *(αk + k) == ( P10 + P11 + … + P1α1 ) , a1 * a2 * … *an == p1α1 *…*pkαk
其中的每一小项用秦九韶算法来计算:while (a – ) t = (t * p + 1) % mod;
简记:【与约数个数算法思路一样:算出约数,个数次幂】 乘积初始值 res :1
main:unordered_map <int,int> primes; {key,次幂}
i <= x / i :满足约数 :primes[i] ++;
判断是否有大于sqrt(n)的约数 :if(n>1)
公式:∑ 约数^(幂+1)^
for(int j;primes[j] != 0;j++) res = res * (primes[j].second + 1) % mod;
#include<bits/stdc++.h>
using namespace std;
#include<algorithm>
#include<unordered_map>
const int mod = 1e9 + 7;
typedef long long ll;
int main()
{
int n;
cin >> n;
unordered_map <int,int> primes;
while(n --)
{
int x;
cin >> x; //累加每个a[i]的质因子个数
for(int i = 2;i <= x / i;i++)
while(x % i == 0)
{
x /= i;
primes[i] ++;//存了所以质因数的指数
}
if(x > 1) primes[x] ++;
}
//约数之和
ll res = 1; //乘积初始值: 1
for(auto prime: primes)
{
int p = prime.first , a = prime.second;
ll t = 1;
while(a--) t = (t * p + 1) % mod; //pi^0 + pi^1 + ... + pi^a1
res = res * t % mod;
}
//for(auto prime: primes) res = res * (prime.second + 1) % mod; 约数个数
cout << res << endl;
return 0;
}
872. 最大公约数
给定n对正整数ai,bi,请你求出每对数的最大公约数。
输入格式
第一行包含整数n。
接下来n行,每行包含一个整数对ai,bi。
输出格式
输出共n行,每行输出一个整数对的最大公约数。
数据范围
1≤n≤105,
1≤ai,bi≤2*109
输入样例:
2
3 6
4 6
输出样例:
3
2
最大公约数:欧几里得算法【辗转相除法】
d | a && d | b ==> d | (a+b) ,d | (ax + by)
最核心 gcd(a,b) == gcd(b,a%b)
证明:a mod b = a - 向下取整[a/b] * b = a - c * b
d | b && d | a + c * b ----- > d | a -----> 最核心成立
库函数 __gcd(a,b) ;两个下杠
#include<bits/stdc++.h>
using namespace std;
用这个:
int gcd(int a,int b)
{
//我写成if(a%b == 0)return b;即下一步b变为0之前,b已是最大公约数,输出
if(b == 0) return a; //a%b == 0也就是b = 0
return gcd(b,a % b);
}
int my_gcd(int a,int b) //测试编译出问题...
{
if(a%b == 0)return b;
return gcd(b,a%b);
}
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\n",&a,&b);
printf("%d\n",gcd(a,b));
}
return 0;
}
欧拉函数
题目描述 :
给定 n 个正整数 ai,请你求出每个数的欧拉函数。
欧拉函数的定义
1~N 中与 N 互质的数的个数被称为欧拉函数,记为 ?(N)。
若在算数基本定理中,N=p1a1 * p2a2… pmam,则:
φ(N) = N×(p1-1)/p1×(p2-2)/p2×…×(pm-1)/pm
输入输出格式 :
输入
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。
输出
输出共 n 行,每行输出一个正整数 ai 的欧拉函数。
输入输出样例 :
输入
3
3
6
8
输出
2
2
4
定义:
给定n个正整数,请你求出每个数的欧拉函数
1~N中与N互质的数的个数为欧拉函数值φ(N)
N = p1a1 * p2*a2 * p3a3 … * pnan; 【先转换成分解质因数形式】
φ(N) = N * (1 - 1 / p1) * (1 - 1 / p2) * … * (1 - 1 / pn) 【与质因子的次数没有关系】
【证明:利用容斥原理结论】
1.从1~N中去掉p1 , p2 , … pk 的所有倍数 【会被去两次,如既是p1的倍数又是p2的倍数,所以要加回来】
2.加上所有pi * pj 的倍数 [i与j为1~k中任意两个数]
3.减去所有pi * pj * pk ;
4.加上所有pi * pj * pk * pl ;
1~N中所有与N互质的个数刚好为容斥原理公式: 减1加2减3加4…减N-1加N
N - N/p1 - N/p2- N/p3- N/p4 … - N / pn 为倍数个数
加 N / (p1p2) + … + N / (p(n-1) * pn )
减N / (p1p2p3) …
加 N / (p1p2p3*p4)
== N * (1 - 1 / p1) * (1 - 1 / p2) * … * (1 - 1 / pn) == φ(N)
简记:欧拉函数φ(N) == N * (1 - 1 / p1) * (1 - 1 / p2) * ... * (1 - 1 / pn)
== N / pi *(p1-1)
N × Π ( p i − 1 p i ) N× \Pi (\dfrac{pi-1}{pi}) N×Π(pipi−1)
#include <iostream>
using namespace std;
#include<algorithm>
int main()
{
int n;
cin >> n;
while(n --)
{
int a;
cin >> a;
int res = a;
for(int i = 2;i <= a / i;i ++) //求质因数
if(a % i == 0)
{
res = res / i * (i - 1); //欧拉函数公式
while(a % i == 0) a /= i; //除去质因数的指数次 ,去除此质因数倍数
}
if(a > 1) res = res / a * (a - 1); //可能有> sqrt(a) 的质因数
}
return 0;
}
874. 线性筛法求欧拉函数
给定一个正整数n,求1~n中每个数的欧拉函数之和。
输入格式
共一行,包含一个整数n。
输出格式
共一行,包含一个整数,表示1~n中每个数的欧拉函数之和。
数据范围
1≤n≤106
输入样例:
6
输出样例:
12
分类讨论:
- i%pj == 0 φ(pj * i) == pj * φ(i) == i * ∑(1 - 1 / p[n])
- i%pj != 0 φ(pj * i) == pj * φ(i) * (1 - 1 / pj) == φ(i) * (pj - 1)
φ ( p j ∗ N ) = p j ∗ φ ( N ) ∗ ( p j − 1 p i ) 约 分 = φ ( N ) ∗ ( P j − 1 ) φ(pj * N) = pj * φ(N) *(\dfrac{pj-1}{pi}) \dfrac{约分}{} = φ(N) * (Pj-1) φ(pj∗N)=pj∗φ(N)∗(pipj−1)约分=φ(N)∗(Pj−1)
//线性筛法 O(n) 【 能求很多... ,如下面就在 if(i % primes[j] == 0)语句变型分类判断 ,在求质因子的过程中顺带求出欧拉函数值
/*
prime[cnt++]=i;:如果发现i是素数,那么就把它放进prime数组里,同时把计数器加一。
prime[j]<=n/i;如果pj乘以i的值大于n就判断完了,【选择成对约数小的 n/a | n - - -> a < n^1/2^】。
内层for循环要做两件事情,分为两个条件判断:
1.如果i除以prime[j](简称pj)不等于0,说明pj不是i的最小质因子,并且可以说明pj比i的最小质因子小。
这样,pji的最小值因子就一定是pj,就先把pji这个数筛掉。
2.如果i除以pj等于0,说明pj是i的最小质因子,而我们如果继续把j++的话,那么下一个pj就一定不是当前i的最小质因子了,为了避免后面重复计算造成错误,所以这里要break。
综上所述,无论pj能否整除i,pj*i一定是合数,一定要筛掉。【约数成对出现】
线性筛法相当于是对埃氏筛法求素数个数的一种优化,线性筛是用最小质因数进行素数筛选,而埃氏筛法则是通过每一个数的倍数进行素数筛选。比如6这个数,用线性筛法只会被筛一次,而用埃氏筛法会分别被2和3各筛一次。
这里引用别的思路2~~:
用线性筛求质数个数的具体做法为 :
在void getPrimes(int n)函数中,最外层依旧是从2遍历到n进行循环,st[i]表示数字i是否被筛除。
每一次循环开始时,若是if(!st[i]), 则primes[cnt ++ ] = i,表明i是个素数。
不管i是否为素数,我们都要再在for (int j = 0; primes[j] <= n / i ; j ++ )这个循环中,对其他数进行筛选。
先让st[primes[j] * i ]= true, 在此可以保证prime[j] 一定是prime[j] * i的最小质因数,
因为后面还有一行代码,if(i % primes[j] = = 0) break :
i % primes[j] == 0 表明primes[j]是i的最小质因数,
如果i % primes[j] != 0 ,prime[j]也一定小于i 的最小质因数,因为primes[j]是从大到小存储素数的。
至于这里为什么要进行break,因为当primes[j]成为i的最小质因数之后,
在下一次循环得到的primes[j + 1] 就不再是i * primes[j + 1] 的最小质因数了,故循环结束。
#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
typedef long long LL;
int primes[N],cnt; //质因子数组 ,统计质因子个数
int phi[N]; //欧拉函数表
bool st[N]; //标记数组
LL get_eulers(int n)
{
phi[1] = 1;//定义出发,1与自己互质
for(int i = 2; i <= n;i++) //线性筛法 + phi代表欧拉函数符号
{
if(!st[i])
{
primes[cnt ++] = i;
phi[i] = i - 1; //为质数,欧拉函数就为i-1
}
for(int j = 0; primes[j] <= n / i;j++)
{
st[primes[j] * i] = true; //筛非质数
if(i % primes[j] == 0) // 倍数遍历完 break;
{
phi[primes[j] * i] = phi[i] * primes[j]; //==0记不用减1
break;
}
phi[primes[j] * i] = phi[i] * (primes[j] - 1);
}
}
LL res = 0; //1~n的欧拉函数之和
for(int i = 1 ;i <= n ; i++) res += phi[i];
return res;
}
int main()
{
int n;
cin >> n;
cout << get_eulers(n) << endl;
return 0;
}
欧拉函数用途:欧拉定理
欧拉定理: aφ(n) 与 n 互质 则 gcd(aφ(n) , n) = 1 或者 说 aφ(n) 与 n mod 1 同余
【学数学要完全自己想,能推出一遍公式,在背了很多熟练的基础上】
费马小定理:
ap-1 与 p 与1同模 == gcd(ap-1,p) = 1
875. 快速幂 O(logk)
给定 n 组 ai,bi,pi,对于每组数据,求出 aibi mod pi 的值。[ mod p 答案就在 1 ~ p -1 之中]
输入格式
第一行包含整数 n。
接下来 n 行,每行包含三个整数 ai,bi,pi。
输出格式
对于每组数据,输出一个结果,表示 aibi mod pi 的值。
每个结果占一行。
数据范围
1≤n≤100000,
1≤ai,bi,pi ≤2 ×109
输入样例:
2
3 2 5
4 3 9
输出样例:
4
1
快速幂 O(logk) 【反复平方法】
ak mod p
1 <= a,k,p <= 1e9
预处理: a20^^ mod p , a21^^ mod p ,a22^^ mod p ,
ak = ∑aij^^ == a2x0+2x1+…+2xn^^ ;
即把ak的拆成 a平方k次
简记:typedef long long LL;
LL qmi(底数,幂,%p)
while(k)
{
if(k & 1)res = (LL)res * a % p; //奇数次幂开始多乘1个a
k >>= 1; 等效 k /= 2; 【 >>= 1 等效 /= 2 】
a = (LL)a * a % p;
}
#include<algorithm>
typedef long long LL;//数论大多题long long ,读入数据也用scanf较好
LL qmi(int a,int k,int p)
{
int res = 1;
while(k) //2^x^ (奇数+ 1) == k
{ //若指数为奇数,多乘一个a
if(k & 1)res = (LL)res * a % p; //奇数次幂开始多乘1个a [也可以写成 k%2]【 if语句在k奇数即 k%2 == 1时执行 】
k >>= 1; //k /= 2;
a = (LL)a * a % p;
}
return res;
}
int main()
{
int n;
scanf("%d",&n);
while(n --)
{
int a,k,p;
scanf("%d%d%d",&a,&k,&p);
printf("%lld\n",qmi(a,k,p));
}
return 0;
}
876. 快速幂求逆元
给定n组ai,pi,其中pi是质数,求ai模pi的乘法逆元,若逆元不存在则输出impossible。
注意:请返回在0~ p - 1 之间的逆元。
乘法逆元的定义
若整数b,m互质,并且b|a,则存在一个整数x,使得a/b≡ax(mod m),则称x为b的模m乘法逆元,记为b(-1) (mod m)。
b存在乘法逆元的充要条件是b与模数m互质。当模数m为质数时,b^(m?2)即为b的乘法逆元。
输入格式
第一行包含整数n。
接下来n行,每行包含一个数组ai,pi,数据保证pi是质数。
输出格式
输出共n行,每组数据输出一个结果,每个结果占一行。
若ai模pi的乘法逆元存在,则输出一个整数,表示逆元,否则输出impossible。
数据范围
1≤n≤105,
1≤ai,pi≤2109
输入样例:
3
4 3
8 5
6 3
输出样例:
1
2
impossible
逆元:
a / b ≡ a * x (mod m)
a / b ≡ a * b逆元 (mod m)
两边同乘b ,a ≡ a * b * b逆元 (mod m)
消去a , b * b逆元 ≡ 1 (mod m) 【性质】
再由费马定理 : bp-1 ≡ 1 (mod p) - - - > 重要结论:b * b p-2 ≡ 1 (mod p) 【证明完毕】
【找到一个x,使得b*x 同余 1 (mod p) ,则可取 x = bp-2】
费马小定理: 如果p是一个质数,而整数a不是p的倍数,则有a(p-1)≡1(mod p) 因此a mod p的逆元是 a(p-2)
求取a(p-2) 可以用快速幂求取
逆元能解决类似:(a / b mod p)类型的问题
简记:qmi(底数,次幂,mod p) 快速幂
if(a % p)printf("%lld",res); else puts("impossible");
因为 a % p == 0时会为res = 1,不正确,应该为impossible ,所以用 a % p
typedef long long LL;
#include<cstdio>
LL qmi(int a,int k,int p)
{
LL res = 1; //初始值为1 !!!
while(k)
{
if(k & 1) res = (LL)res * a % p; //强制转换
k >>= 1;
a = (LL)a * a % p;
}
return res;
}
int main()
{
int n;
scanf("%d",&n);
while(n --)
{
int a,k,p;
scanf("%d%d",&a,&p); //已知推导公式: a逆元求法: a * a^p - 2 ^ ≡ 1 (mod p) 即k = p - 2
LL res = qmi(a,p-2,p);//a逆元 == a^p-2^ % p
if(a % p)printf("%lld",res); //因为 a % p == 0时会为res = 1,不正确,应该为impossible ,所以用 a % p
else puts("impossible");
}
return 0;
}
877. 扩展欧几里得算法
(递归,裴蜀定理,gcd)
给定 n 对正整数 ai,bi,对于每对数,求出一组 xi,yi,
使其满足 ai * xi+bi * yi=gcd(ai,bi)。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含两个整数 ai,bi。
输出格式
输出共 n 行,对于每组 ai,bi,求出一组满足条件的 xi,yi,每组结果占一行。
本题答案不唯一,输出任意满足条件的 xi,yi 均可。
数据范围
1≤n≤105,
1≤ai,bi≤2×109
输入样例:
2
4 6
8 18
输出样例:
-1 1
-2 1
拓展欧几里得算法
时间复杂度:O(n*log(a+b))
①拓展欧几里得算法解决的问题:对于任意给定的两个正整数a、b,求解x,y使得ax+by=(a,b) (a,b)的意思:a和b的最大公约数
②问题引入:裴蜀定理
给定任意一对正整数a、b,存在非零整数x、y,使得ax+by=(a,b)
扩欧作用:求解方程 ax+by=gcd(a,b) 的解(x、y)、求逆元 等
推理:
①当 b=0 时 ax+by=a 所以 x=1,y=0
②当 b≠0 时
设gcd(a,b)=gcd(b,a%b)=d
又
by′+(a%b)x′=gcd(b,a%b)
by′+(a–a/b–b)*x’=gcd(b,a%b)
b(y’-[a/b]x’)+ax’=gcd(b,a%b)=gcd(a,b)=d
所以
x=x′(不变),y=y’ - [a/b]x’(更新)
要用scanf,这时候连cin解除同步都比它慢得多
//本题需要用子问题的结果来计算当前问题的结果,所以需要等子问题算完之后再算
裴蜀定理:
对任意一对正整数 a,b,那么一定存在非零整数x,y【构造法证明】,使得ax+by = gcd(a,b) 即a,b能凑出来的最小的正整数
构造法证明:即欧几里得算法:
通解
x = x0 - (b/a) * k , k∈Z
y = y0 + (b/a) * k , k∈Z
int gcd(int a,int b)
{
if(b == 0)return a; //0和任何数的最大公约数都是那个数本身
return gcd(b,a%b);
}
int exgcd(int a,int b,int &x,int &y) //扩展欧几里得算法
{
if(!b) //若b = 0 ,返回 x = 1,y = 0
{
x = 1,y = 0;
return a;
}
int d = exgcd(b,a%b,y,x); //把a,b颠倒了,x,y对应系数翻转
y -= a / b * x;
return d;
}
int main()
{
int n;
scanf("%d",&n);
while(n --)
{
int a,b,x,y;//根据题意,让x,y引用传递 exgcd( int a,int b , int &x,int &y);
scanf("%d%d",&a,&b);
exgcd(a,b,x,y);
printf("%d %d\n",x,y);
}
return 0;
}
应用:求线性同余方程 ax 与 b 同余 (mod m)
给定n组数据ai,bi,mi,对于每组数求出一个xi,使其满足ai?xi≡bi(mod mi),如果无解则输出impossible。
输入格式
第一行包含整数n。
接下来n行,每行包含一组数据ai,bi,mi。
输出格式
输出共n行,每组数据输出一个整数表示一个满足条件的xi,如果无解则输出impossible。
每组数据结果占一行,结果可能不唯一,输出任意一个满足条件的结果均可。
输出答案必须在int范围之内。
数据范围
1≤n≤105,
1≤ai,bi,mi≤2?109
输入样例:
2
2 3 6
4 3 5
输出样例:
impossible
-3
exgcd求解线性同余方程
如果 a?x≡b(mod m)
那么存在 y∈Z 使得 a?x+m?y=b
令d=gcd(a,m)
exgcd(a,m,x0,y0) 得a?x0+m?y0=d
if(b % d == 0)则有解
等式两边同乘 b/d 得
b/d(a?x0+m?y0)=b
a?(b/d?x0)+m?(b/d?y0)=b
x=b/d?x0
int main()
{
int n;
scanf("%d",&n);
while(n --)
{
int a,b,m;
scanf("%d%d%d",&a,&b,&m);
int x,y;
int d = exgcd(a,m,x,y);
if(b % d) puts("impossible"); //d不是b的倍数,无解
else printf("%d\n",(LL)x * (b / d) % m);
}
return 0;
}
高斯消元 - 解方程 [暂放 不能写出0/1]
题目描述:
输入一个包含n个方程n个未知数的线性方程组。方程组中的系数为实数。求解这个方程组。
下图为一个包含m个方程n个未知数的线性方程组示例:
输入格式
第一行包含整数n。
接下来n行,每行包含n+1个实数,表示一个方程的n个系数以及等号右侧的常数。
输出格式
如果给定线性方程组存在唯一解,则输出共n行,其中第i行输出第i个未知数的解,结果保留两位小数。
如果给定线性方程组存在无数解,则输出“Infinite group solutions”。
如果给定线性方程组无解,则输出“No solution”。
数据范围
1≤n≤100,
所有输入系数以及常数均保留两位小数,绝对值均不超过100。
输入样例:
3
1.00 2.00 -1.00 -6.00
2.00 1.00 -3.00 -9.00
-1.00 -1.00 2.00 7.00
输出样例:
1.00
-2.00
3.00
能在O(n3)内求解n个线性方程
a1x1 + a2x2 +a3x3 +a4x4 + … an * xn = b1;
aa1 * xx1 + … = b2;
. . . . . . .
①消去后为完美阶梯型 唯一解
②0 = 非零 , 无解
③0 = 0 ,无穷多组解
方程系数矩阵行列式【行列式基本运算】:
①交换某两行
②把某行乘上一个非零的数
③把某行乘上n倍加到某行
枚举每一列C
①找到当前绝对值最大的一行
②再把此行放到第一行
③将该行第一个数变成1
④将下面所有行的第C列消成0 【成倍数 ,行直接相加减】 ,重复【再找最大值,放到第二行 ,再把第一个数变成1,再加减 】
⑤最终变成上三角行列式 ,值 == 对角线的积
如
1 0.5 -1.5 -4.5
0 1 1/3 -1
0 0 1 3
x1 + 0.5 * x2 + -1.5 * x3 = -4.5
x2 + 1/3 * x3 = -1
x3 = 3
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int N = 110;
const double eps = 1e-6;
int n;
double a[N][N];
int gauss() {
int c, r;
for (c = 0, r = 0; c < n; c ++ ) {
int t = r;
for (int i = r; i < n; i ++ ) {//找到当前列绝对值最大的那一项系数
if (fabs(a[i][c]) > fabs(a[t][c])) {
t = i;
}
}
if (fabs(a[t][c]) < eps) continue;//最大值为0说明所有数都为0,之前存在这一列的约数方程
for (int i = c; i < n + 1; i ++ ) swap(a[t][i], a[r][i]);//将最大系数的那一列换到最上面
for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];//从表达式末尾进行计算,将找到最大值的那一列的系数变成1,
for (int i = r + 1; i < n; i ++ ) {//将当前列的所有数消成0
if (fabs(a[i][c]) > eps) {
for (int j = n; j >= c; j -- ) {
a[i][j] -= a[r][j] * a[i][c];
}
}
}
r ++ ; //对下一行进行操作
}
if (r < n) {//不是唯一解 或 无解
for (int i = r; i < n; i ++ ) {
if (fabs(a[i][n]) > eps) {//无解
return 2;
}
}
return 1;// 有无穷多组解
}
for (int i = n - 1; i >= 0; i -- ) {//方程存在唯一解 ,从下往上回代,得到方程的解
for (int j = i + 1; j < n; j ++ ) {
a[i][n] -= a[j][n] * a[i][j];
}
}
return 0; //唯一解
}
int main() {
cin >> n;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < n + 1; j ++ ) {
cin >> a[i][j];
}
}
int t = gauss();
if (t == 0) {
for (int i = 0; i < n; i ++ ) printf("%.2lf\n", a[i][n]);
}
else if (t == 1) cout << "Infinite group solutions" << endl;
else cout << "No solution" << endl;
return 0;
}
885. 求组合数 I C(m,n) 【dp】
题目描述 :
给定 n 组询问,每组询问给定两个整数 a,b,请你输出 C(b,a) mod (109+7) 的值。
数据范围 :
1 ≤ n≤10000,
1 ≤ b ≤ a ≤ 2000 【审题C(b,a) a >= b】
输入输出格式 :
输入
第一行包含整数 n。
接下来 n 行,每行包含一组 a 和 b。
输出
共 n 行,每行输出一个询问的解。
输入输出样例 :
输入
3
3 1
5 3
2 2
输出
3
10
1
思路:DP递推式预处理 C(a,b)
C(a,b) = C(a-1,b) + C(a-1,b-1)
选苹果 - 分类 – 每个包含/不包含
【到a的情况对一个未选择的苹果有两种结果,第a个选它,或不选它,选其他的(那么就要从它之外的再选一个)】
[离散数学-接近实际问题-离散-高等代数 - 数学分析 - 均需三学期 !!!]
const int N = 2010 , mod = 1e9 + 7;
int c[N][N];
void init()
{
for(int i = 0;i < N;i ++)
for(int j = 0;j <= i;j ++)
if(!j) c[i][j] = 1; //定义: C(a,0) = 1
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1] % mod); //DP 选/不选
}
int main()
{
init();
int n;
scanf("%d",&n);
while(n --)
{
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",c[a][b]);
}
return 0;
}
886 求组合数 II 【数据大小10万级别】 【费马小定理+快速幂+逆元】
题目描述:
给定n组询问,每组询问给定两个整数a,b,请你输出C(a,b) mod (10^9+7)的值。
输入格式
第一行包含整数n。接下来n行,每行包含一组a和b。
输出格式
共n行,每行输出一个询问的解。
数据范围
1≤n≤100000,1≤b≤a≤105
输入样例:
3
3 1
5 3
2 2
输出样例:
3
10
1
思路: 【即C(a,b)[a>=b]组合公式展开 —> C(a-1,b) * C(a-1,b-1) ,分别求分母和分子】
结合代码:
fact[i] = i! % p;
infact[i] = (i!)的逆元 % p 【除 ==> 乘逆元(快速幂 – 费马小定理)】 【分母 】
C( a , b ) % p = fact[a % p] % p * infact[b - a] % p * infact[b] % p
C a b = a ! b ! ( a − b ) ! C^b_a = \dfrac{a!}{b!(a-b)!} Cab=b!(a−b)!a!
typedef long long LL;
const int N = 100010,mod = 1e9 + 7;
int fact[N],infact[N]; //i! :阶乘 , i阶乘的逆元 : infact[i]
int qmi(int a,int k,int p) //数值大就LL 一般都是!!!
{
int res = 1; //乘除法的初始值
while(k)
{
if(k % 2) res = (LL)res * a % p; //强转 也可以k & 1 表示奇数就行
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int main()
{
fact[0] = infact[0] = 1; //乘除法的初始值
for(int i = 1;i < N;i++)
{
fact[i] = (LL)fact[i - 1] * i % mod;//i的阶乘
infact[i] = (LL)infact[i - 1] * qmi(i,mod - 2,mod) % mod; //a % p 的逆元 的幂 = p-2 ,a逆为a^p-2^
}
int n;
scanf("%d",&n);
while(n --)
{
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",(LL)fact[a] * infact[b] % mod * infact[a - b] % mod); //%f等于0原因分母求解过程中0,后一直为0,最后相乘结果为0
}
return 0;
}
887. 求组合数 III 【le18级别】 【卢卡斯定理 + 逆元 + 快速幂 】
给定n组询问,每组询问给定三个整数a,b,p,其中p是质数,请你输出C b上a下 mod p的值。
输入格式
第一行包含整数n。
接下来n行,每行包含一组a,b,p。
输出格式
共n行,每行输出一个询问的解。
数据范围
1≤n≤20,
1≤b≤a≤1e18,
1≤p≤105,
输入样例:
3
5 3 7
3 1 5
6 4 13
输出样例:
3
3
2
解题思路:
a和b的取值到了1018 改用:
卢卡斯定理: C ( a , b ) % p = C( a%p , b%p ) * C( a/p , b/p ) % p ; (p为质数)
#include<algorithm>
typedef long long LL;
int p;
int qmi(int a,int k) //p是全局变量不用传进来,直接用
{
int res = 1;
while(k)
{
if(k % 2) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
//特别注意题目给定输入的a,b顺序
int C(int a,int b) //组合数 C(a,b) == b! / (a-b)! * a! ********
{
int res = 1;
for(int i = 1,j = a ;i <= b;i++,j--) // i为b! ,j = a! ,qmi求i逆元 为
{
res = (LL)res * j % p; //除 , 乘上i的逆元
res = (LL)res * qmi(i,p-2) % p;
}
return res;
}
int lucas(LL a,LL b) //递归 //传进的数是LL类型
{
if(a < p && b < p) return C(a,b);
return (LL)C(a % p,b % p) * lucas(a / p, b / p) % p;
}
int main()
{
int n;
cin >> n;
while(n --)
{
LL a, b;
cin >> a >> b >> p ;
cout << lucas(a,b) << endl;
}
}
888.求组合数 IV 【没有%p – 高精度算出准确结果】 【分解质因数 + 高精度乘法 --只用一次高精度提高运行效率】
较难
题目描述:
输入a,b,求C(a,b)的值。注意结果可能很大,需要使用高精度计算。 【a >= b】
输入格式
共一行,包含两个整数a和b。
输出格式
共一行,输出C(a,b)的值。
数据范围
1≤b≤a≤5000
输入样例:
5 3
输出样例:
10
*公式:C(a,b) = a! / ( b! (a - b)! ) 【a >= b】
一般的处理方式:先把C(a,b)分解质因数,变成只有乘法的形式 --只要高精度乘法 -优化运算速度
质因子p个数运算: 分母里面的有多少个p, - 分子里的多少个p ,差值就是个数
阶乘当中包含p的个数 a! = 向下取整a/p1 [p1为p的倍数] +向下取整a/p2 [p2为p2的倍数] +向下取整a/p3 + …
质数的次数【p的各种倍数中拿出一个p 得出所有含有p的项中,p的总个数】【如p的k次方被加k次,不重复不漏算 】
#include<vector>
const int N = 5010;
int primes[N],cnt;
bool st[N];
int sum[N];
void get_primes(int n) //线性筛法
{
for(int i = 2;i <= n;i++)
{
if(!st[i]) primes[cnt ++] = i;//没有标记,需遍历i的质数的倍数 ,筛除 [第一次i为2,是质数先放入,后面3,5同理,4就被筛掉了]
for(int j = 0 ;primes[j] <= n / i;j++) //遍历到 i == sqrt(n)
{
st[primes[j] * i] = true; //筛除i的质数的倍数,
if(i % primes[j] == 0) break; // i是pj 的倍数时,后面已经筛选完了,都是倍数,就不用继续了,结束
}
}
}
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();i++)
{
t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
while(t)//t == 0结束
{
c.push_back( t % 10);
t /= 10;
}
return c;
}
int main()
{
int a,b;
cin >> a >> b;
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]); // (res = 1) * primes[i] 的^sum[i]^次方
for(int i = res.size() - 1;i >= 0;i --) printf("%d", res[i]);//vector遍历输出
puts("");
return 0;
}
889.满足条件的01序列 【卡特兰数-用法极多!】
C a t ( n ) = C 2 n n − C 2 n n − 1 = C 2 n n n + 1 Cat(n)= {C_{2n}^n} - {C_{2n}^{n-1}} = \dfrac{C_{2n}^n}{n+1} Cat(n)=C2nn−C2nn−1=n+1C2nn
题目描述:
给定n个0和n个1,它们将按照某种顺序排成长度为2n的序列,求它们能排列成的所有序列中,
能够满足任意前缀序列中0的个数都不少于1的个数的序列有多少个。输出的答案对10^9+7取模。
输出格式
共一行,包含整数n。
输出格式
共一行,包含一个整数,表示答案。
数据范围
1≤n≤105
输入样例:
3
输出样例:
5
思路
路径转换成一个排列 ,如坐标从(0,0)开始走,0向右走,1向上走,到终点得到一个序列
当要求能走的坐标满足x >= y 时,ans = = 即所有不经过y = x + 1这条边的数 = = 所有走法 - 经过y = x + 1这条边的数
如(0,0)- - >(6,6) 必走12步且选6步向上走 y = x + 1等效,即C(12,6) - C(12,5)
扩展推: (0,0) 到 (n,n) 且不经过y = x + 1 即
Cat(n) = C(n+n , n) - C(n+n , n-1) = C(2*n,n) / (n + 1) 【卡特兰数】
【卡特兰数应用:栈的合法序列,括号匹配数量…
简记: qmi(x的逆元) ,在%p条件下可以表示为 1 / x
main :
①②求C(2n,n) ①先求 a! / (a-b)!
for(int i = a;i > a - b;i --) res = (LL)res * i % mod; //a! / (a-b)!
②再求 1 / b!
for(int i = 1;i <= b;i ++) res = (LL)res * qmi(i,mod - 2,mod) % mod;
③最后再乘 1 / (n+1) 【用 n+1 的逆元表示】
res = (LL)res * qmi(n + 1,mod - 2,mod) % mod;
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;//取模为质数
//因为%p所以都可以int
ll qmi(int a,int k,int p) //如果p不是质数,那么逆元只能用扩展欧几里得定理求!!!
{
int res = 1;
while(k)
{
if(k % 2) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int main()
{
int n;
cin >> n;
int a = 2 * n , b = n;
int res = 1;
//求C(2*n,n) = a! / b!(a-b)! 【C(a,a-b) * b!逆元(分母)】
for(int i = a;i > a - b;i --) res = (LL)res * i % mod; //a! / (a-b)!
for(int i = 1;i <= b;i ++) res = (LL)res * qmi(i,mod - 2,mod) % mod;
//C(2*n,n) / (n+1)
res = (LL)res * qmi(n + 1,mod - 2,mod) % mod; // 乘(n+1) 逆元 == 1/(n+1) % p
cout << res << endl;
return 0;
}
【其他代码-dp法参考】
【【 卡特兰数 : Cat(n) = C(2n, n) / (n + 1)】】
c[a][b]仅能求 1 <= a,b <= 2000
C
a
t
(
n
)
=
C
2
n
n
−
C
2
n
n
−
1
=
C
2
n
n
n
+
1
Cat(n)= {C_{2n}^n} - {C_{2n}^{n-1}} = \dfrac{C_{2n}^n}{n+1}
Cat(n)=C2nn−C2nn−1=n+1C2nn
const int N = 1e4+10;
int c[N][N]; //存组合数 c[a][b] 表示从a个苹果中选b个的方案数 【a >= b】
void init() //init数组,组合数
{
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个时是最后一个还是前面的(最后一个取/不取)
}
int main()
{
cin >> n;
cout << c[2*n][n] / (n+1) ;
return 0;
}
分解质因数法求组合数 —— 模板题 AcWing 888. 求组合数 IV
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
1. 筛法求出范围内的所有质数
2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + …
3. 用高精度乘法将所有质因子相乘
890.能被整除的数(容斥原理)
最简单容斥原理:【韦恩图-三个互相相交的圆-分成7个部分】
概率论的加法公式【加上减去重复的】 p(ABC) = p(A) + p(B) + p© - p(AB) - p(BC) - p(AC) + p(ABC)
|S1 + S2 + S3| = |S1| + |S2| + |S3| - |S1^S2| - |S2^S3| - |S1^S3| +|S1S2S3| (||符号表示集合的元素个数)
容斥原理公式: 扩展到n ∑si - ∑|si1^si2|【所有任意两项和】 + ∑|si1si2si3| + … + (-1)m-1 ∑|s1s2s3^… ^sm|
总共的方案数: C(n,1) + C(n,2) + C(n,3) +… C(n,n) = 2n - 1 ,再加C(n,0) = = 2n - - - 时间复杂度
= = 从n个中选出任意多个数的方案数
还可用数学归纳法证明 【学离散数学上下册,等包含全部的编程原理】
给定一个整数n 和m个不同的素数(质数) p 1 ~ p_m ,求1 ~ n 1中能被 p 1 ~ p_m 之一整除的整数有多少个。
输入格式:
第一行包含整数n和m。第二行包含m个质数。
输出格式:
输出一个整数,表示满足条件的整数的个数。
数据范围:
1 <= m <= 16
1 <= n,pi <= 109
输入样例:
10 2
2 3
输出样例:
7
(样例:1-10中能被质数2或3整除的个数有7个)
思路: 利用容斥原理减少时间复杂度 O(2x)
质数的倍数构成的集合
举例:如1~10中能被2,或3整除的个数
S2 = {2,4,6,8,10} , S3 = {3,6,9} , S2^S3 = {6}
|S2+S3| = |S2| + |S3| - |S2^S3| = 5 + 3 - 1 = 7
注意单独求每一项的时间复杂度
容斥原理补充恒等式:
C(k,1) - C(k,2) +… + (-1)k-1C(k,k) = 1 - C(k,0) = 0
1~n中p的倍数的个数 = 向下取整n/p ,能被2,3同时整除的个数 即能被6整除的个数 = 向下取整 n/6
能被p1 … pk个数同时整除的元素的个数 = 向下取整 n/p1p2 … pk
O(m*2m) 【int默认向下取整】
求的是|Sp1+Sp2+Sp3+Sp4+Sp5+Sp6+…+Spk| = 容斥原理 【符号取决于项里有多少个集合,奇数个集合为正 “+”】
简单实现方式: 枚举所有集合情况 - 位运算 1 ~ 2n-1
for(int i =1;i < 2n;i++) 把i看成n位
简记:
从m个质数中用位运算操作选 ,【1~2^m^- 1 ,每二进制位0/1即: 选/不选】。
比如m = 2时(质数有2,3),数6 = 11选中了第1,2个质数(2,3)
【计算过程t = 1 乘质数看能否等于枚举的整数,cnt(选取集合/质数个数)确定符号】
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 20;
int n,m;
int p[N];
int main() //所有选法集合个数 , 集合个数奇数符号为负 【 1 ~ 2^n - 1 】(每个选不选)
{
//读入
cin >> n >> m;
for(int i = 0;i < m;i ++) cin >>p[i];
int res = 0;
for(int i = 1;i < 1 << m;i++) // 1 ~ 2^m -1 枚举
{
int t = 1,cnt = 0;
for(int j = 0;j < m;j++)
if(i >> j & 1)//判断第j位是不是1 i >> k & 1
{
cnt ++ ; //判断是奇数还是偶数个集合 ,确定符号
if((LL)t * p[j] > n ) //大于n,不用算了
{
t = -1;
break;
}
t *= p[j]; //乘积 < n
}
if( t != -1)
{
if(cnt % 2) res += n / t; //集合个数为奇数,符号为正
else res -= n / t;
}
}
cout << res << endl;
return 0;
}
【博弈论】 --> Nim游戏【台阶 - 集合 - 拆分】
NIM游戏 【可以转化成有向图游戏】
公平组合游戏ICG
有向图游戏
Mex运算
891.Nim游戏
题目描述:
给定n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。问如果两人都采用最优策略,先手是否必胜。
输入格式
第一行包含整数n。第二行包含n个数字,其中第 i 个数字表示第 i堆石子的数量。
输出格式
如果先手方必胜,则输出“Yes”。否则,输出“No”。
数据范围
1≤n≤105,1≤每堆石子数 ≤109
输入样例:
2
2 3
输出样例:
Yes
分析:
先给出Nim游戏的一些概念和结论:
给定N堆物品,第i堆物品有Ai个。
两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,
可把一堆取光,但不能不取。取走最后一件物品者获胜。
两人都采取最优策略,问先手是否必胜。
我们把这种游戏称为NIM博弈。
把游戏过程中面临的状态称为局面。
整局游戏第一个行动的称为先手,第二个行动的称为后手。
若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。
同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。
定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0
也就是说,当各堆石子的个数a1 ^ a2 ^ … ^ an = 0时,先手必败,否则先手必胜。
引用~~
我们先直观的去理解这个定理,对于第i堆石子,其石子个数为x,在x的二进制表示中,至多能为每位贡献一个1.。
比如32位的整数,每一位至多有一个1,这是显然的。a1 ^ a2 ^ … ^ an = 0说明了在二进制的各位,1出现的次数都是偶数的。
先手不论怎么拿,对二进制的各位而言,要么不影响某位1的总数,要么让某位1的总数多1或者少1。
而后手只需要从某堆中拿的石子个数使得石子堆中各位1的总和恢复成偶数即可,
由于各位1的总数开始是偶数,先手不可能将该位的1拿完(除非先手面临的就是必败态:石子已经被拿完了),
所以后手永远有石子可拿,因此先手必败。不妨举个例子,为了表示方便,石子的个数都用二进制表示。
设石子一共有3堆,个数分别为1111,1001,0110,可见初始状态的异或和是0。
假设先手先从第一堆里拿走了1100,第一堆还剩0011,此时二进制中各位1的个数从左往右分别是1,1,2,2.。
为了让前两位的1的个数恢复偶数,后手先想到从某堆里拿走1100,即与先手进行同样的操作,
但是发现此时各堆石子的数目为0011,1001,0110,没有哪个石子堆可以满足后手的需要,于是后手就想将二进制中1的个数变为0,2,2,2。
即从第二堆石子中拿出100个石子,则第二堆还剩0111个石子。
此时二进制各位1的总数又恢复偶数了,重复这类操作,直至先手面临的状态是0,0,0,0,即必败态。
上面不是对这个定理进行严格的证明,只是想从直观的角度去解释这个定理。
下面正式证明该定理:首先必败态,没有石子了,异或和是0。
然后对于任意一种异或和非0的情况,我们都可以用一步操作使之变为异或和为0的情况。
设a1 ^ a2 ^ … ^ an = x != 0,
设x二进制最高位的1是在第k位,则n堆石子中至少有一堆石子的个数该位为1,设第i堆石子个数ai第k位为1,
则ai^x < ai,(将ai的第k为从1变成0了,低位无论怎么变也不会超过原来ai的大小)。
此时我们从第i堆中拿走ai - ai^x个石子,
则第i堆石子个数变为ai - (ai - ai^x)= ai ^ x个,故a1 ^ … ^ ai ^ x ^ … ^an = a1 ^ a2 ^ … ^ an ^ x = x ^ x = 0。
对于任意一种异或和为0的情况,我们拿走任意数量的石子都无法使得石子的异或和还是0.
假设从第i堆拿走一定数量的石子使得ai变为ai’,a1 ^ … ^ ai’ ^ … ^ an = 0,
又a1 ^ … ^ ai ^ … ^ an = 0,将两个式子左右两边分别异或起来得ai’ ^ ai = 0,故ai’ = ai,也就是什么都不拿,这是违背规则的,
故假设不成立。对于异或和为0的状态,我们无法使之一步转化为异或和还是0的状态。
现在我们找到了最终的必败态是石子数量为0,异或和为0.只要先手面对的是异或和非0的状态,
则可以通过一步操作使之转化为异或和为0的状态,后手面对异或和为0的状态无论怎么操作都只能转化为异或和非0的状态,
因此只要先手每次都采用最优策略,面临必败态的一定是后手。
动态规划
①01背包
每种物品仅有一件,只能用一次 【每件选或不选】
②完全背包
每种物品有无限个
③多重背包
每种物品有限个
④分组背包
每一组里面选n件物品(选了苹果就不能选香蕉)
注意:【不一定要装满背包】
⑤混合背包(前四种的加和)
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
闫式DP法 :
状态表示 f(i,j)
①表示的集合是什么
所有选法的一个集合
一个集合满足条件 ①只从前i个物品中选 ②总体积 <= j
②集合的属性是什么 ,一般三种:最大值、最小值、元素的数量
状态计算
集合的划分 ,f(i,j)表示的所有的选法分成两大类 ,
包含第i个的放左子集/不包含第i个的放右集合(放/不放)第i个
右边可能不存在,装不下的时候,为空集
【两个集合包括所有元素,可能需要不重复】
思维过程:先想朴素二维,再等价优化成一维
先把所有选法的第i个物品去掉(所有选法相对排序大小不变(就如全班加上一个分数,第一名还是第一名))
朴素
#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 main()
{
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++) //0~v[i]之前放不下,没有意义
{
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]); //右边状态
}//【j-1中选出的最大值+选了i后+w[i]即】
cout << f[n][m] << endl;
return 0;
}
一维优化
#include <iostream>
using namespace std;
#include<algorithm>
const int N = 1010;
int n,m; //种类n , 体积m
int v[N],w[N];//体积,价值
int dp[N]; //体积取m时,最大价值dp[m]
//【由去掉一维,发现状态==i-1,不对,则j从m开始到v[i],j-v[i]还没有更新,等效i-1的,可降成一维】
//【可优化原因】:分两种讨论用的j和j-v[i] <= j且i的用f(i-1)状态,滚动
//转化成一维: - 滚动数组
int main()
{
cin >> n >> m ;
for(int i = 1;i <= n;i++) scanf("%d%d", &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; //体积取m时,最大价值dp[m]
return 0;
}
边输入边处理:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int f[MAXN];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
int v, w;
scanf("%d%d", &v, &w); // 边输入边处理
for(int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}
完全背包问题
有 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
朴素
#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N],w[N];
int dp[N];
int f[N][N]; //爆栈直接导致无法编译 。。。各种问题
int n,m;
int main()
{
cin >> n >> m ;
for(int i = 1;i <= n;i++) scanf("%d%d", &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;
}
一维优化
简记:以每种物品最多选i个分组 :(从每种物品至多选i = 1种开始,即i从1开始)
价值优化成j=v[i]开始(v[i]之前放不下)
n:物品种类 ,m:背包体积
#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int dp[N];
int main()
{
cin >> n >> m ;
for(int i = 1;i <= n;i++) scanf("%d%d", &v[i], &w[i]);
for(int i = 1;i <= n;i++)
for(int j = v[i];j <= m;j ++)//01背包改为正序即为完全背包【好记】
dp[j] = max(dp[j] , dp[j - v[i] ] + w[i]);
cout << dp[m] <<endl; //m体积时存最大价值
return 0;
}
多重背包问题 I
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
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<bits/stdc++.h>
#include<algorithm>
const int N = 1010;
int v[N],w[N],s[N];
int dp[N];
int f[N][N]; //爆栈直接导致无法编译 。。。各种问题
int n,m;
#include<algorithm>
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 = 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;
}
多重背包问题 II 【拆分转01背包】
有 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
简记:【二进制优化】
7 的二进制111 :3位选不选0/1
1 2 4可组合成 0-7
但取 s = 10 ,非2^n - 1
就用 s - 1 - 2 - 4 = 3 < 8 ,1 10 100 加上余数得到所有遍历
1 2 4 3 -- >枚举 (全不选) 0 - 10 (全选)
#include<iostream>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[M]; // 体积<M
int main()
{
cin >> n >> m; //n件种类组合,m总体积
int cnt = 0; //记录拆分后种类 ,最后 n = cnt
for(int i = 1;i <= n;i++)
{
int a,b,s;
cin >> a >> b >> s; //不能用v,w和数组重名,改用a,b!!!
int k = 1; //拆分初始项1 ,k *= 2 1 2 4 (1 10 100)...
while(k <= s)
{
cnt ++ ;
v[cnt] = a * k; // 原来共 a * s 拆成 a * 1 + a * 2 + a * 4 ....
w[cnt] = b * k;
s -= k;
k *= 2; 也可以 k <<= 1 (快一些)
}
if(s > 0)
{
cnt ++ ;
v[cnt] = a * s; // 最后若非 2^n-1 , 取 s 与 (2^n-1) 的余数 ,如 10 ,1 2 4 ... 3 ,最后一项 3v,3w [即可表示所有值
w[cnt] = b * s;
}
}
n = cnt; //拆分后的种类总数增加变为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;
}
分组背包问题
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
集合: 只从前i组物品中选,且总体积不大于j的所有选法
简记:第i组选哪个物品(可以不选)(i从1开始,可不选k从0开始)s[i]:每组内的物品编号
w[i][k] ,第i组第k件物品价值 , v[i][j] 第i组第k件物品体积 ,f[j]:占用体积为j时的max
注意一维优化(同01背包)j逆序才符合公式 f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
for(int i = 1;i <= n;i++) //第i组
for(int j = m;j >= 0;j--) //j体积
for(int k = 0;k < s[i];k++)
if(v[i][k] <= j) //能放得下才更新 每组中的第k件物品价值
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
const int N = 110;
int n,m;
int v[N][N],w[N][N],s[N];//枚举每一组的物品
int f[N];
int main()
{
cin >> n >> m; //n价值 ,m体积
for(int i = 1;i <= n;i++)
{
cin >> s[i]; //每组
for(int j = 0;j < s[i];j++) //含有不选(相当于多加一个空物品,等效k,从0开始)
cin >> v[i][j] >> w[i][j];
}
for(int i = 1;i <= n;i++) //第i组
for(int j = m;j >= 0;j--) //j体积
for(int k = 0;k < s[i];k++)
if(v[i][k] <= j) //能放得下才更新 每组中的第k件物品价值
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[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 main()
{
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. 最长上升子序列 O(n2)
给定一个长度为 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
简记:f[i] : 以第i个数结尾的最长上升子序列 LIS
if(a[j] < a[i]) f[i] = max(f[i] , f[j] + 1)
DP时间复杂度:O(n2)
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n;
int a[N],f[N];
int main()
{
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;
}
896.最长上升子序列 II O(nlogn)
题目描述:
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数N。第二行包含N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤100000,-109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
【贪心+二分解法】维护严格单调上升序列q
解释的不错:每个LIS的最后一个数一定 > (长度小于这个LIS)的子序列 的最后一个数
简记:q[i]存所有小于a[i]的最大值,二分模板 : 找小于a[i]的max
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
int a[N];
int q[N];
int n;
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
q[0]=-2e9;//绝对最小,可被更新
int len=0;
for(int i=0;i<n;i++)
{
int l=0,r=len;
while(l<r)
{
int mid=(l+r+1)>>1; //单调找小于a[i]的max
if(q[mid]<a[i]) l=mid; //发现满足时,ans在mid右边 ,选取模拟二
else r=mid-1;
}
len=max(len,r+1);
q[r+1]=a[i];
}
printf("%d\n",len);
return 0;
}
897.最长公共子序列
给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数N和M。
第二行包含一个长度为N的字符串,表示字符串A。
第三行包含一个长度为M的字符串,表示字符串B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1 ≤ N,M ≤ 1000
输入样例:
4 5
acbd
abedc
输出样例:
3
动态规划
状态表示f[i,j]
集合 第一个序列的前i个字母,和第二个序列的前j个字母的子序列
属性 Max
状态计算
00 01 10 11 (ai 和 bj 包括/不包括在最长子序列中 的四种情况–> 转移到当前状态 取最大值max)
00【ai和bj均不出现在公共子序列中】: f[i-1][j-1]
11:f[i-1][j-1] + 1 , 01: f[i - 1][j] 包含了【ai不出现在公共子序列当中,bj出现的情况】 , 10: f[i][j-1]
包含可以代替的原因:我们求的是最大公共长度,即重复没有关系,答案不变 【因为f[i-1][j-1]被包含在01,10这两种情况中,所有不用比较】
f[i][j] = max(f[i-1][j] , f[i][j - 1] );
if(a[i] == b[j]) f[i][j] = max(f[i][j] , f[i-1][j-1] + 1);
杂扩:y总char改int的分析(逐位赋值,拼接成整数)
简记:两个序列,每个位选/不选 (0 / 1) * (0 / 1) [四种分类]
【用到下标i-1,所以从1开始】 char a[N]
情况包含有重复,分开比,max不影响(类似max(a,b) , max(b,c) )
f[i][j] = max(f[i-1][j] , f[i][j - 1] );
if(a[i] == b[j]) f[i][j] = max(f[i][j] , f[i-1][j-1] + 1);
#include <bits/stdc++.h>
#include<algorithm>
using namespace std;
const int N = 1010;
int n,m;
char a[N],b[N];
int f[N][N];
int main()
{
scanf("%d%d",&n,&m);
scanf("%s%s",a + 1 , b + 1);
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
{
f[i][j] = max(f[i-1][j] , f[i][j - 1] );
if(a[i] == b[j]) f[i][j] = max(f[i][j] , f[i-1][j-1] + 1);
}
cout << f[n][m] <<endl;
return 0;
}
902.最短编辑距离
给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:
删除–将字符串 A 中的某个字符删除。
插入–在字符串 A 的某个位置插入某个字符。
替换–将字符串 A 中的某个字符替换为另一个字符。
现在请你求出,将 A 变为 B 至少需要进行多少次操作。
输入格式
第一行包含整数 n,表示字符串 A 的长度。
第二行包含一个长度为 n 的字符串 A。
第三行包含整数 m,表示字符串 B 的长度。
第四行包含一个长度为 m 的字符串 B。
字符串中均只包含大写字母。
输出格式
输出一个整数,表示最少操作次数。
数据范围
1≤n,m≤1000
输入样例:
10
AGTCTGACGC
11
AGTAAGTAGGC
输出样例:
4
简记:所有将a[1~i]变成b[1~j]的操作方式
初始化
for (int i = 0; i <= m; i ++ ) f[0][i] = i; // 只添加,a的前0个添加i个后与b的前i个字母相等
for (int i = 0; i <= n; i ++ ) f[i][0] = i; // 只删除,a的前i个删除后变成与b的前0个相等
删除和插入操作比较
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
替换操作比较
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]); 替换改f[i-1][j-1]+0/1次,a[i]==b[j]则最后一次不用改,不加操作次数
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
scanf("%d%s", &n, a + 1);//用a+1地址读入,字符串下标从1开始【用i-1,不用特判边界】
scanf("%d%s", &m, b + 1);
// 初始化
for (int i = 0; i <= m; i ++ ) f[0][i] = i; // a的前0个与吧的前i个字母:只用添加使得前i个字母相等
for (int i = 0; i <= n; i ++ ) f[i][0] = i; // 只删除,a的前i个删除后变成与b的前0个相等
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
// 删除和插入操作比较
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
// 替换操作比较
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);//若相等就是最后一次,不用再加
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
printf("%d\n", f[n][m]);
return 0;
}
899. 编辑距离
给定n 个长度不超过10 的字符串和m 次询问,每次询问含一个字符串和一个操作次数m ,问这n 个字符串里有多少个可以通过不超过m 次操作变成询问的那个字符串。单个字符的插入、删除或替换算一次操作。
输入格式:
第一行包含两个整数n 和m 。接下来n 行,每行包含一个字符串,表示给定的字符串。再接下来m 行,每行包含一个字符串和一个整数,表示一次询问。字符串中只包含小写字母,且长度均不超过10 。
输出格式:
输出共m 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。
数据范围:
1 ≤ n , m ≤ 1000
3 2
abc
acd
bcd
ab 1
acbd 2
输出样例:
1
3
简记:有次数限制limit的最短编辑距离(裸题)
#include <iostream>
#include <string.h>
using namespace std;
const int N = 15, M = 1010;
int n, m;
int f[N][N];
char str[M][N];//字符串数组 (取字符串:str[i])
int edit_dis(char a[], char b[]) { //编辑距离DP模板
int la = strlen(a), lb = strlen(b); //长度
for (int i = 0; i <= lb; i++) f[0][i] = i;
for (int i = 0; i <= la; i++) f[i][0] = i;
for (int i = 1; i <= la; i++)
for (int j = 1; j <= lb; j++) {
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if (a[i - 1] == b[j - 1]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
return f[la][lb];
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> str[i];
while (m--) {//询问n个串中有几个满足
char s[N];
int limit;
scanf("%s%d", s, &lim); //引用型 , char数组可直接%s读入 !!!
int res = 0;
for (int i = 0; i < n; i++)
if (edit_dis(str[i], s) <= limit) //能在限制次数limit内从a[1~i]->b[1~j]的方案数
res++;
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) : 3003 < 1e8 ,可行
简记:前缀和s[j] - s[i-1]:i->j区间的石子合并值 len = r - l + 1 (从区间长度为2 , len = 2开始)
for (int i = 1;i + len - 1 <= n; i++) {
int j = i + len - 1; f[i][j] = 0x3f3f3f3f; 注意求min,初始化INF
for (int k = i; k <= j; k++) f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]); k遍历[l,r]
合并果子:优先队列(小根堆),与此题答案不一样(此题只能合并相邻的)
#include <bits/stdc++.h>
using namespace std;
const int N = 310;
int n;
int s[N];
int f[N][N];
int main() {
scanf("%d", &n);
for (int i = 1;i <= n; i++) scanf("%d", &s[i]), s[i] += s[i - 1] ; //以分号结束算一条语句
//memset(dp, 0, sizeof dp);
for (int len = 2; len <= n; len++) //len == r - l + 1
for (int i = 1;i + len - 1 <= n; i++) {
int j = i + len - 1; f[i][j] = 0x3f3f3f3f;
for (int k = i; k <= j; k++) f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);// 搬动[l,r] 的代价为 s[r] - s[l - 1]
}
printf("%d\n", f[1][n]); //1 -> n 的最小代价
return 0;
}
旧版:
#include<bits/stdc++.h>
using namespace std;
const int N = 310;
int n;
int s[N]; //前缀和数组 , 代价值用利用前缀和数组求区间和 计算
int f[N][N];
int main()
{
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] = 0x3f3f3f3f;//初始化,否则每次都是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,n]区间合并的最小代价为f[1][n]
return 0;
}
递归的DP --> 又称为记忆化搜索
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
完全背包写法:
从1~i中选,总体积恰好是j的选法(化简推导):f[i][j] = f[i-1][j] + f[i][j-1] ;
一维优化:f[j] = (f[j] + f[j - i]) % mod;
#include<bits/stdc++.h> //完全背包考虑
using namespace std;
const int N = 1010,mod = 1e9 + 7;
int n;
int f[N];
int main()
{
cin >> n;
f[0] = 1;
for(int i = 1;i <= n;i ++)
for(int j = i;j <= n;j++)
f[j] = (f[j] + f[j - i]) % mod; //划分成不同价值
cout << f[n] << endl;
return 0;
}
动态规划
状态表示
集合:所有总和是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];
#include<bits/stdc++.h>
using namespace std;
const int N = 1010,mod = 1e9 + 7;
int n;
int f[N][N];
int main()
{
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;
}
338. 计数问题(数位统计dp)
给定两个整数 a 和 b,求 a 和 b 之间的所有数字中0~9的出现次数。
例如,a=1024,b=1032,则 a 和 b 之间共有9个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032
其中‘0’出现10次,‘1’出现10次,‘2’出现7次,‘3’出现3次等等…
输入格式
输入包含多组测试数据。
每组测试数据占一行,包含两个整数 a 和 b。
当读入一行为0 0时,表示输入终止,且该行不作处理。
输出格式
每组数据输出一个结果,每个结果占一行。
每个结果包含十个用空格隔开的数字,第一个数字表示‘0’出现的次数,第二个数字表示‘1’出现的次数,以此类推。
数据范围
0<a,b<100000000
思路:(区间次数和 转化成: 边界的前缀次数和相减)
实现函数 count(n,x) 1~n中x出现的次数
count(b,x) - count(a-1,x) :【区间[a,b]】x出现的次 数
291.蒙德里安的梦想 (状压DP-二进制位压缩)
memset在cstring库
求把NM的棋盘分割成若干个12的的长方形,有多少种方案。
例如当N=2,M=4时,共有5种方案。当N=2,M=3时,共有3种方案。
如下图所示:
输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数N和M。
当输入用例N=0,M=0时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1≤N,M≤11
输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205
j 取 0 ~ 2n-1 (横着的为1)
上一个状态转移过来所有情况:
①不能为连续横着(会重叠不可能) :
②且空的为竖着的(那么每个竖着的需要连续占用2行):
cnt存储连续0的个数(j没选横为0:即放竖的连续占的行)
简记:难
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 12,M = 1 << N; // 2^N
int n,m;
ll f[N][M];
bool st[M];
int main() {
int n,m;
while(cin >> n >> m , n || m ) //读到 0 0 结束
{
memset(f,0,sizeof f);
for(int i = 0;i < 1 << n;i++) //所有状态是否不存在连续奇数个0
{
st[i] = true;//假设成立
int cnt = 0; //当前这一段连续的0的个数
for(int j = 0;j < n;j++)
if(i >> j & 1) //i的第j位是1时
{
if(cnt & 1) st[i] = false; //奇数个,状态不合法,false
cnt = 0; //此段遇到1结束了,归零
}
else cnt ++ ; //不是1
if(cnt & 1) st[i] = false; //再判最后一段
}
f[0][0] = 1;
for(int i = 1;i <= m;i++)
for(int j = 0;j < 1 << n;j++) //二进制
for(int k = 0;k < 1 << n;k++)
if((j & k) == 0 && st[j | k]) //(j & k)==0横着伸出不冲突下一格 且 j | k 不存在连续奇数个0 ,才可以转移
f[i][j] += f[i-1][k];
cout << f[m][0] << endl;
}
return 0;
}
高精度压位
const int base = 1e9;
就是把高精度add模板中 /10 、%10 的换成 /base 、 % base;
可运用:130. 火车进出栈问题