AcWing——算法基础课
第一讲 基础算法
快速排序
快速排序
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5+7;
int q[N];
//快速排序模板
void quickSort(int q[], int l, int r){
if(l>=r) return;
int i = l -1,j = r + 1,x=q[l+r>>1];
while(i < j){
do i++; while(x > q[i]);
do j--; while(x < q[j]);
if(i<j) swap(q[i],q[j]);//容易忽略if条件
}
quickSort(q,l,j);
quickSort(q,j+1,r);
}
int main(){
int n;
cin>>n;
for(int i = 0;i<n;i++) scanf("%d",&q[i]);
quickSort(q,0,n-1);
for(int i = 0;i<n;i++) printf("%d ",q[i]);
return 0;
}
归并排序
1.归并排序
#include<iostream>
using namespace std;
const int N = 1e5+7;
int q[N],tmp[N];
//归并排序模板
void mergeSort(int q[],int l , int r){
if(l>=r) return;
int mid = l+r>>1;
mergeSort(q,l,mid),mergeSort(q,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++];
}
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[i] = tmp[j];//i是从l开始,容易写成0
}
int main(){
int n;
cin>>n;
for(int i = 0; i< n;i++) scanf("%d",&q[i]);
mergeSort(q,0,n-1);
for(int i = 0; i< n;i++) printf("%d ",q[i]);
return 0;
}
2.逆序对的数量
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 1e5+7;
int q[N],tmp[N];
LL mergeSort(int q[],int l ,int r){
if(l>=r) return 0;
int mid = l+r>>1;
LL x = mergeSort(q,l,mid)+mergeSort(q,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++];
x+=mid-i+1;
}
}
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[i]=tmp[j];//i是从l开始,容易写成0
return x;
}
int main(){
int n;
cin>>n;
for(int i =0;i<n;i++) cin>>q[i];
cout<<mergeSort(q,0,n-1)<<endl;
return 0;
}
二分
1.数的范围(整数二分)
//整数二分法模板
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
#include <iostream>
using namespace std;
const int N = 100010;
int q[N],n,m;
int main(){
cin>>n>>m;
for(int i = 0; i < n ; i++) cin>>q[i];
while(m--){
int x;
cin>>x;
int l = 0,r= n-1;
while(l<r){
int mid=l+r>>1;
if(x<=q[mid]) r = mid;
else l = mid+1;
}
if(q[l]!=x) cout<<"-1 -1"<<endl;
else{
cout<<l<<" ";
int l = 0 ,r =n-1;
while(l<r){
int mid=l+r+1>>1;
if(x>=q[mid]) l = mid;
else r = mid-1;
}
cout<<l<<endl;
}
}
return 0;
}
2.数的三次方根(浮点数二分)
//浮点数二分模板
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
#include<iostream>
using namespace std;
int main(){
double n;
cin>>n;
double l = -10000,r=10000;
while(r-l>1e-8){
double mid = (l+r)/2;
if(n<= mid *mid *mid) r=mid;
else l= mid;
}
printf("%lf\n",l);
return 0;
}
高精度
1.高精度加法
#include<iostream>
#include<vector>
using namespace std;
//高精度加法模板
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++){
if(i < A.size()) t+=A[i];
if(i < B.size()) t+=B[i];
C.push_back(t % 10);
t/=10;
}
if(t) C.push_back(1);
return C;
}
int main(){
string a,b;
cin>>a>>b;
vector<int> A,B;
for(int i = a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
for(int i = b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
vector<int> C = add(A,B);
for(int i = C.size()-1;i>=0;i--) cout<<C[i];
cout<<endl;
return 0;
}
2.高精度减法
#include<iostream>
#include<vector>
using namespace std;
bool cmp(vector<int> &A,vector<int> &B){
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){
vector<int> C;
int t = 0;
for(int i = 0;i<A.size();i++){
t = A[i]-t;
if(i<B.size()) t-=B[i];
C.push_back((t+10)%10);//合二为一
if(t<0) t=1;
else t=0;
}
while(C.size()>1 && C.back()==0) C.pop_back();//去掉前导0
return C;
}
int main(){
string a,b;
cin>>a>>b;
vector<int> A,B;
for(int i = a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
for(int i = b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
vector<int> C;
if(cmp(A,B)) C = sub(A,B);
else {
C = sub(B,A);
cout<<"-";
}
for(int i = C.size()-1;i>=0;i--) cout<<C[i];
cout<<endl;
return 0;
}
3.高精度乘法
#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++){
if(i<A.size()) t+=A[i]*b;
C.push_back(t % 10);
t/=10;
}
while(C.size()>1 && C.back()==0) C.pop_back();//去掉前导0
return C;
}
int main(){
string a;
int b;
cin>>a>>b;
vector<int> A;
for(int i = a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
vector<int> C = mul(A,b);
for(int i = C.size()-1;i>=0;i--) cout<<C[i];
cout<<endl;
return 0;
}
4.高精度除法
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
//高精度除法模板(高精度除以低精度)
vector<int> div(vector<int> &A,int b,int &t){
vector<int> C;
t = 0;
for(int i = A.size() - 1; i>=0; i--){
t = t*10+A[i];
C.push_back(t / b);
t %= b;
}
reverse(C.begin(),C.end());//C是正序的,要先反转
while(C.size()>1 && C.back()==0) C.pop_back();//去掉前导0
return C;
}
int main(){
string a;
int b;
int t;//余数
cin>>a>>b;
vector<int> A;
for(int i = a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
vector<int> C = div(A,b,t);
for(int i = C.size() - 1;i >= 0;i--) cout<<C[i];
cout<<endl;
cout<<t<<endl;
return 0;
}
前缀和与差分
1.前缀和(一维)
//一维前缀和公式
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
#include <iostream>
using namespace std;
const int N = 1e5+10;
int a[N],s[N];
int main(){
int n,m;
cin>>n>>m;
for(int i = 1; i <= n ; i++) cin>>a[i];
for(int i = 1; i <= n ; i++) s[i] = s[i-1]+a[i];//前缀和数组
while(m--){
int l,r;
cin>>l>>r;
cout<<s[r] - s[l-1]<<endl;
}
return 0;
}
2.子矩阵的和(二维)
//二维前缀和公式
S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
#include <iostream>
using namespace std;
const int N = 1e3+10;
int a[N][N],s[N][N];
int main(){
int n,m,q;
cin>>n>>m>>q;
for(int i = 1 ;i<= n ;i++)
for(int j = 1 ;j<= m ;j++)
cin>>a[i][j];
//初始化前缀和数组
for(int i = 1 ;i<= n ;i++)
for(int j = 1 ;j<= m ;j++)
s[i][j] = s[i][j-1]+s[i-1][j]-s[i-1][j-1]+a[i][j];//关键公式
while(q--){
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
cout<<s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]<<endl;//关键公式
}
return 0;
}
3.差分(前缀和的逆运算)
//一维差分
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
#include<iostream>
using namespace std;
const int N = 1e5+10;
int a[N],b[N];
void insert(int l ,int r, int c){
b[l]+=c;
b[r+1]-=c;
}
int main(){
int n,m;
cin>>n>>m;
for(int i = 1 ;i <=n;i++) cin>>a[i];//a是前缀和数组
for(int i = 1 ;i <=n;i++) insert(i,i,a[i]);//初始化b数组
//对b数组进行操作
while(m--){
int l,r,c;
cin>>l>>r>>c;
insert(l,r,c);
}
for(int i = 1 ;i <=n;i++) a[i] = a[i-1] +b[i];//对b数组操作完后重新给a赋一遍值
for(int i = 1 ;i <=n;i++) cout<<a[i]<<" ";
cout<<endl;
return 0;
}
4.差分矩阵
//二维差分
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
#include<iostream>
using namespace std;
const int N = 1e3+10;
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;
b[x1][y2+1]-=c;
b[x2+1][y1]-=c;
b[x2+1][y2+1]+=c;
}
int main(){
cin>>n>>m>>q;
for(int i = 1 ; i <= n;i++)
for(int j = 1 ; j <= m;j++)
cin>>a[i][j];
//由前缀和数组a初始化出差分数组b
for(int i = 1 ; i <= n;i++)
for(int j = 1 ; j <= m;j++)
insert(i,j,i,j,a[i][j]);
while(q--){
int x1,y1,x2,y2,c;
cin>>x1>>y1>>x2>>y2>>c;
insert(x1,y1,x2,y2,c);
}
//b数组加好后重新给前缀和数组a赋一遍值
for(int i = 1 ; i <= n;i++)
for(int j = 1 ; j <= m;j++)
a[i][j] = a[i-1][j]+a[i][j-1]-a[i-1][j-1]+b[i][j];
//输出结果
for(int i = 1 ; i <= n;i++){
for(int j = 1 ; j <= m;j++)
cout<<a[i][j]<<" ";
cout<<endl;
}
return 0;
}
双指针算法
1.最长连续不重复子序列
//双指针算法模板
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
#include<iostream>
using namespace std;
const int N = 1e5+10;
int s[N],a[N];
int main(){
int n;
cin>>n;
for(int i = 0 ;i < n ; i ++) cin>>a[i];
int res = 1;//最长连续不重复子序列的长度
for(int i=0,j=0;i<n;i++){
s[a[i]]++;
while(s[a[i]]>1){
s[a[j]]--;
j++;
}
res = max(res,i-j+1);
}
cout<<res<<endl;
}
2.数组元素的目标和
#include<iostream>
using namespace std;
const int N = 1e5+10;
int a[N],b[N];
int main(){
int n,m,x;
cin>>n>>m>>x;
for(int i =0 ;i<n;i++) cin>>a[i];
for(int i =0 ;i<m;i++) cin>>b[i];
//当a[i]+b[j]>x时,j往左走,b[j]变小,和也变小;当a[i]+b[j]<x时,i往右走,a[i]变大,和也变大;总结:i就是只能往右走的,j就是只能往左走的
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){
cout<<i<<" "<<j<<endl;
break;
}
}
return 0;
}
3.判断子序列
#include<iostream>
using namespace std;
const int N = 1e5+10;
int a[N],b[N];
int main(){
int n,m;
cin>>n>>m;
for(int i =0 ; i<n;i++) cin>>a[i];
for(int i =0 ; i<m;i++) cin>>b[i];
int i =0,j=0;
while(i<n&&j<m){
if(a[i] == b[j]) i++;
j++;
}//以上while循环其实也可以写成双指针算法模板里的for循环格式,只是i和j的作用域的问题
if(i == n) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
位运算
二进制中1的个数
//位运算模板
//1.求n的二进制中的第k位数字(从最低位开始数,最低位是第0位,先让x的二进制右移k位,再和1的二进制数进行与运算;比如n=101010000,k=4,就是要取101010000的第四位数字也就是1,先右移4位成10101,再和00001与运算得1,任何数与1进行与运算都得最低位)
int result = n>>k&1;
//2.返回n的二进制的从最低位开始数的第一位1及低位的所有数字(比如101010000就是返回10000),如果 x 的二进制表示是正数,那么 -x 的二进制表示就是 x 的补码,即x与x得补码进行与运算,比如101010000,即101010000&010110000,得10000
int lowbit(int x){
return x&-x;
}
#include<iostream>
using namespace std;
const int N = 1e5+10;
int a[N];
int lowbit(int x){
return x&-x;
}
int main(){
int n;
cin>>n;
for(int i = 0 ; i< n ; i++) cin>>a[i];
for(int i = 0 ; i< n ; i++){
int t = 0;
while(a[i]){
a[i]-=lowbit(a[i]);
t++;
}
cout<<t<<" ";
}
cout<<endl;
}
离散化
区间和
//离散化模板
vector<int> alls;//存储所有待离散化的值
sort(alls.begin(),alls.end();//将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());//去掉重复元素
//二分求出x对应的离散化的值
int find(int x)//找到第一个大于等于x的位置
{
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,2,.n,因为前缀和一般下标是从1开始的
}
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 3e5+10;
typedef pair<int,int> PII;
int a[N],s[N];//a[N]是存储插入坐标的值的数组,并且插入坐标和a的索引相对应,s是a的前缀和数组,上图中的a和s数组是从下标1开始画的(图有点错误)
vector<int> alls;//alls是存储所有坐标的数组,上图中的alls数组是从下标0开始画的,所以find那里要return的r要加1,才能正确映射到a和s数组(图有点错误)
vector<PII> add,query;//add是存储插入坐标和插入值的容器,query是存储查询坐标的容器
//返回alls数组中x值的位置
int find(int x){
int l = 0, r = alls.size() -1;
while(l<r){
int mid = l+r>>1;
if( x <= alls[mid]) r = mid;
else l = mid+1;
}
return r+1;
}
int main(){
int n,m;
cin>>n>>m;
while(n--){
int x,c;
cin>>x>>c;
add.push_back({x,c});
alls.push_back(x);
}
while(m--){
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());
//根据alls中存储的坐标将要加的值映射到a数组中
for(int i = 0; i < add.size();i++){
int x = find(add[i].first);
a[x]+=add[i].second;
}
for(int i = 1;i <= alls.size(); i++) s[i]= s[i-1] + a[i];
//计算区间和
for(int i = 0; i < query.size();i++){
int l = find(query[i].first);
int r = find(query[i].second);
cout<<s[r]-s[l-1]<<endl;
}
return 0;
}
区间合并
区间合并
//区间合并模板
//将所有存在交集的区间合并
void merge(vector<PII> &segs){
vector<PII> res;
sort(segs.begin(),segs.end());
int st = -2e9, ed = -2e9;
for(int i = 0 ; i< segs.size(); i++){
if(segs[i].first > ed){
st = segs[i].first,ed = segs[i].second;
res.push_back({st,ed});
}
else ed = max(ed,segs[i].second);
}
segs = res;
}
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 1e5+10;
typedef pair<int,int> PII;
void merge(vector<PII> &segs){
vector<PII> res;
sort(segs.begin(),segs.end());
int st = -2e9, ed = -2e9;
for(int i = 0 ; i< segs.size(); i++){
if(segs[i].first > ed){
st = segs[i].first,ed = segs[i].second;
res.push_back({st,ed});
}
else ed = max(ed,segs[i].second);
}
segs = res;
}
int main(){
vector<PII> segs;
int n;
cin>>n;
while(n--){
int l,r;
cin>>l>>r;
segs.push_back({l,r});
}
merge(segs);
cout<<segs.size()<<endl;
return 0;
}
第二讲 数据结构
单链表
单链表
//单链表模板
int head,idx;//head指头指针(头指针指向头结点),e[head]指的是头结点的值
//idx指的是下一个可存储的位置的索引,也可以说是插入的第几个数的序号,即idx=0时表示插入的第一个数
int e[N],ne[N];//e[N]存储的是结点的值,ne[N]存储的是结点的下一个结点的索引
void init(){
head = -1;//头指针默认赋-1
idx = 0;
}
void insert_to_head(int x){
e[idx] = x;
ne[idx] = head;
head = idx++;
}
void remove(int k){
ne[k] = ne[ne[k]];
}
void insert(int k ,int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
//此模板的关键在于k-1索引上的数绝对是插入的第k个数
#include<iostream>
using namespace std;
const int N = 1e5+10;
int head,idx;//head指头指针(头指针指向头结点),e[head]指的是头结点的值
//idx指的是下一个可存储的位置的索引,也可以说是插入的第几个数的序号,即idx=0时表示插入的第一个数
int e[N],ne[N];//e[N]存储的是结点的值,ne[N]存储的是结点的下一个结点的索引
void init(){
head = -1;//head的默认值赋-1
idx = 0;
}
void insert_to_head(int x){
e[idx] = x;
ne[idx] = head;
head = idx++;
}
void remove(int k){
ne[k] = ne[ne[k]];
}
void insert(int k ,int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
int main(){
int m;
cin>>m;
init();
while(m--){
char op;
int k,x;
cin>>op;
if(op == 'H'){
cin>>x;
insert_to_head(x);
}
else if(op == 'I'){
cin>>k>>x;
insert(k-1,x);//第k个插入的数索引为k-1,因为idx是从0开始的,第一个插入的数索引为1;
}else{
cin>>k;
if(!k) head = ne[head];
else remove(k-1);
}
}
for(int i = head ; i != -1 ; i = ne[i]) cout<<e[i]<<" ";
cout<<endl;
return 0;
}
双链表
双链表
//双链表模板
int l[N],r[N],e[N];
int idx;
void init(){
//头结点指向尾结点,尾结点指向头节点,头结点指针为0,尾结点指针为1,所以头指针为1,尾指针为0
r[0] = 1;
l[1] = 0;
idx = 2;
}
void insert(int k, int x){
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;//必须这步在前面,否则r[k]的值会被修改
r[k] = idx++;
}
void remove(int k){
r[l[k]] = r[k];
l[r[k]] = l[k];
}
#include<iostream>
using namespace std;
const int N = 1e5+10;
int l[N],r[N],e[N];
int idx;
void init(){
//头结点指向尾结点,尾结点指向头节点,头结点指针为0,尾结点指针为1,所以头指针为1,尾指针为0
r[0] = 1;
l[1] = 0;
idx = 2;
}
void insert(int k, int x){
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;//必须这步在前面,否则r[k]的值会被修改
r[k] = idx++;
}
void remove(int k){
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main(){
int m;
cin>>m;
init();
while(m--){
string op;
int k,x;
cin>>op;
if(op == "L"){
cin>>x;
insert(0,x);//最左端插入表示索引1的位置插入数
}
else if( op == "R"){
cin>>x;
insert(l[1],x);//最右端插入表示尾结点前面的位置插入数
}
else if( op == "D"){
cin>>k;
remove(k + 1);//第k个插入的数索引为k+1,因为idx是从2开始的,第一个插入的数索引为2;
}
else if( op == "IL"){
cin>>k>>x;
insert(l[k+1],x);
}
else{
cin>>k>>x;
insert(k + 1 ,x);
}
}
for(int i = r[0] ; i!= 1 ;i = r[i]) cout<<e[i]<<" ";
cout<<endl;
return 0;
}
栈
1.模拟栈
//栈模板
// top表示栈顶
int stk[N], top = -1;
// 向栈顶插入一个数
stk[ ++ top] = x;
// 从栈顶弹出一个数
top -- ;
// 栈顶的值
stk[top];
// 判断栈是否为空,如果 top >= 0,则表示不为空
if (top >= 0)
{
}
#include<iostream>
using namespace std;
const int N = 100010;
int top = -1;//初始时top等于-1,表示没有元素
int stk[N];
int main(){
int m;
cin>>m;
while(m--){
string s;
int x;
cin>>s;
if(s == "push"){
cin>>x;
stk[++top] = x;
}
else if(s == "pop") top--;
else if(s == "empty"){
if(top >= 0) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
else cout<<stk[top]<<endl;
}
return 0;
}
2.表达式求值
#include <iostream>
#include <stack>
#include <string>
#include <map>
using namespace std;
//双栈加优先级表
stack<int> num;
stack<char> op;
map<char, int> h = { {'+', 1}, {'-', 1}, {'*',2}, {'/', 2} };
//求值函数
void eval()
{
int b = num.top();//第二个操作数
num.pop();
int a = num.top();//第一个操作数
num.pop();
char p = op.top();//运算符
op.pop();
int r = 0;//结果
//计算结果
if (p == '+') r = a + b;
else if (p == '-') r = a - b;
else if (p == '*') r = a * b;
else if (p == '/') r = a / b;
num.push(r);//结果入栈
}
int main()
{
string s;//读入表达式
cin >> s;
for (int i = 0; i < s.size(); i++)
{
char c = s[i];
//1.数字,数字入栈
if (isdigit(c))
{
//此while循环是在读到了数字时读完一整个数字
int x = 0, j = i;
while (j < s.size() && isdigit(s[j]))
{
x = x * 10 + s[j] - '0';
j++;
}
num.push(x);
i = j - 1;
}
//2.左括号,左括号无优先级,直接入栈
else if (c == '(')//左括号入栈
{
op.push(c);
}
//3.右括号,括号特殊,遇到左括号直接入栈,遇到右括号不进栈,直接计算
else if (c == ')')
{
while(op.top() != '(')
eval();
op.pop();//左括号出栈
}
//4.运算符,主要是优先级的问题
else
{
while (op.size() && h[op.top()] >= h[c])//待入栈运算符优先级低,则先计算
eval();
op.push(c);//算完之后此运算符入栈
}
}
while (op.size()) eval();//剩余的进行计算
cout << num.top() << endl;//输出结果
return 0;
}
队列
模拟队列
//模拟普通队列模板
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
// 向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++ ;
// 队头的值
q[hh];
// 判断队列是否为空,如果 tt >= hh,则表示不为空
if (tt >= hh)
{
}
//模拟循环队列模板
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;
// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;
// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;
// 队头的值
q[hh];
// 判断队列是否为空,如果hh != tt,则表示不为空(其实不能这样简单判断是否为空)
if (hh != tt)
{
}
#include <iostream>
using namespace std;
const int N = 100010;
int q[N];
//[hh, tt] 之间为队列(左闭右闭)
int hh = 0;//队头位置
int tt = -1;//队尾位置,存第一个元素时的索引为0,所以从0开始存储
int m;
string s;
int main(){
cin >> m;
while(m--){
cin >> s;
//入队
if(s == "push"){
int x;
cin >> x;
q[++tt] = x;
}
//出队
else if(s == "pop"){
hh++;
}
//问空
else if(s == "empty"){
if(tt >= hh) cout << "NO" << endl;
else cout << "YES" << endl;
}
//查询
else if(s == "query"){
cout << q[hh] << endl;
}
}
}
单调栈
单调栈
//单调栈常见模型:找出每个数左边离它最近的比它大/小的数
int top = -1;
while(n--)
{
while (top >= 0 && check(stk[top], x)) top -- ;
stk[ ++ top] = x;
}
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N];
int top = -1;
int main(){
int n;
cin>>n;
while(n--){
int x;
cin>>x;
while(top >= 0 && stk[top] >= x) top--;//比x大的元素全弹出来
if(top >= 0) cout<<stk[top]<<" ";//弹完之后如果还有元素就输出栈顶元素
else cout<<-1<<" ";//无就输出-1
stk[++top] = x;//最后要把x存入,因为x是有可能比之后的元素小的,并且x离之后的元素最近
}
return 0;
}
单调队列
滑动窗口
//单调队列常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
q[ ++ tt] = i;
}
#include <iostream>
using namespace std;
const int N = 1000010;
int a[N],q[N];//q[N]存的是下标
int main(){
int n,k;
cin>>n>>k;
for(int i = 0; i< n ; i++) cin>>a[i];
//最小值
int hh = 0, tt = -1;
for(int i = 0 ; i < n; i++){
//滑出队头
if(hh <= tt && q[hh] < i-k+1) hh++;//i-k+1为窗口的最左端索引
//去掉比当前要进入的数更大的数
while(hh <= tt && a[q[tt]] >= a[i]) tt--;
//当前操作数的索引存入数组
q[++tt] = i;
// i 等于 k - 1 时,窗口的大小刚好为 k。因此,当 i 大于或等于 k - 1 时,窗口就已经包含了至少 k 个元素。
if(i >= k-1) cout<<a[q[hh]]<<" ";
}
cout<<endl;
//最大值,除a[q[tt]] <= a[i]外无变化
hh = 0, tt = -1;
for(int i = 0 ; i < n; i++){
if(hh <= tt && q[hh] < i-k+1) hh++;
while(hh <= tt && a[q[tt]] <= a[i]) tt--;
q[++tt] = i;
if(i >= k-1) cout<<a[q[hh]]<<" ";
}
cout<<endl;
return 0;
}
KMP
KMP字符串
手动模拟求next数组:
对 p = “abcab”
p | a | b | c | a | b |
---|---|---|---|---|---|
下标 | 1 | 2 | 3 | 4 | 5 |
next[] | 0 | 0 | 0 | 1 | 2 |
对next[ 1 ] :前缀 = 空集—————后缀 = 空集—————next[ 1 ] = 0;
对next[ 2 ] :前缀 = { a }—————后缀 = { b }—————next[ 2 ] = 0;
对next[ 3 ] :前缀 = { a , ab }—————后缀 = { c , bc}—————next[ 3 ] = 0;
对next[ 4 ] :前缀 = { a , ab , abc }—————后缀 = { a . ca , bca }—————next[ 4 ] = 1;
对next[ 5 ] :前缀 = { a , ab , abc , abca }————后缀 = { b , ab , cab , bcab}————next[ 5 ] = 2;
// s[]是长文本,p[]是模式串,m是s的长度,n是p的长度
//求模式串的Next数组:
for (int i = 2, j = 0; i <= n; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= m; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == n)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
//KMP算法即字符串匹配算法
#include <iostream>
using namespace std;
const int N = 10010, M = 100010;
//p,s,ne数组索引都是从1开始的
char p[N],s[M];
int ne[N];
int main(){
int n,m;
cin>>n>>p+1>>m>>s+1;
// //输出为空
// cout<<p<<endl;
// cout<<s<<endl;
//
// //输出整个p和整个s
// cout<<p+1<<endl;
// cout<<s+1<<endl;
//求ne数组的过程,ne[1]为0,所以从2开始,ne[i]表示从1到i之间,前缀和后缀最长相等元素的长度
//next数组的求法是模板串自己与自己进行匹配
for(int i = 2, j = 0; i <= n; i++){
//如果不等就回溯
while(j && p[i] != p[j+1]) j = ne[j];
//如果相等,模板串下标j的元素是提前和文本串对应j的下一个元素也就是下标为i的元素比较,所以j要+1
if(p[i] == p[j+1]) j++;
//现在j就已经是前缀和后缀最长相等元素的长度,所以要赋给ne[i]
ne[i] = j;
}
//kmp匹配过程,模板串与文本串进行匹配
for(int i = 1, j = 0; i <= m; i++){
//如果不等就回溯
while(j && s[i] != p[j+1]) j = ne[j];
//如果相等,模板串下标j的元素是提前和文本串对应j的下一个元素也就是下标为i的元素比较,所以j要+1(短的为模板串)
if(s[i] == p[j+1]) j++;
//j == n是找到了完整的相同的字符串
if(j == n){
cout<<i-n<<" ";//输入匹配完全的字符串的头下标,题目下标是从0开始的,所以不用加一
j = ne[j];//继续回溯找下一个相同的字符串,不要从头开始
}
}
return 0;
}
Trie
1.Trie字符串统计
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的字符串数量
// 插入一个字符串
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
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];
}
return cnt[p];
//Trie树快速存储字符集合和快速查询字符集合
#include <iostream>
using namespace std;
const int N = 100010;
//son[][]存储子节点的位置,也存储第几个结点(idx的值),分支最多26条;
//cnt[p]存储以p节点结尾的字符串个数(同时也起标记作用)
//idx表示当前要插入的节点是第几个,每创建一个节点值+1
int son[N][26], cnt[N], idx;
char str[N];
void insert(char *str)
{
int p = 0; //类似指针,指向当前节点
for(int i = 0; str[i]; i++)
{
int u = str[i] - 'a'; //将字母转化为数字
if(!son[p][u]) son[p][u] = ++idx; //该节点不存在,创建节点
p = son[p][u]; //使“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];
}
return cnt[p]; //返回字符串出现的次数
}
int main()
{
int m;
cin >> m;
while(m--)
{
char op[2];
cin>>op>>str;
if(*op == 'I') insert(str);
else cout<<query(str)<<endl;
//改成char op; op == 'I'也行
}
return 0;
}
2.最大异或对
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010,M=31*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位数字
if(!son[p][u]) son[p][u]=++idx; ///如果插入中发现没有该子节点,开出这条路
p=son[p][u]; //指针指向下一层
}
}
//异或运算相同为0不同为1
int search(int x)
{
int p=0;int res=0;
for(int i=30;i>=0;i--)
{
int u=x>>i&1; //从最大位开始找
if(son[p][!u]) //如果当前层有对应的不相同的数,res左移一位并加一
{
p=son[p][!u];
res=res*2+1;
//*2相当左移一位 然后如果找到对应位上不同的数res+1
}
else //如果当前层有对应的相同的数,res左移一位并加零 //刚开始找0的时候是一样的所以+0 到了0和1的时候原来0右移一位,判断当前位是同还是异,同+0,异+1
{
p=son[p][u];
res=res*2+0;
}
}
return res;
}
int main()
{
int n;
cin>>n;
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])); ///search(a[i])查找的是a[i]值的最大与或值
}
cout<<res<<endl;
return 0;
}
并查集
并查集:
1.将两个集合合并
2.询问两个元素是否在一个集合当中
基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储
它的父节点,p[x]表示x的父节点
问题1:如何判断树根:if(p[x]==x)
问题2:如何求x的集合编号:while(p[x]!=x)x = p[x]
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号。p[x]=y
(1)朴素并查集:
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
(2)维护size的并查集:
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
(3)维护到祖宗节点距离的并查集:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
1.合并集合
#include<iostream>
using namespace std;
const int N=100010;
int p[N];//存储父节点的数组,p[x]=y表示x的父节点为y
//找根节点(集合编号)的函数 ,查找过一遍后所有经过的x的父节点的父节点都会变为集合编号以降低时间复杂度
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);//只有根节点的p[x]=x,所以要一直找直到找到根节点
return p[x];//找到了便返回根节点的值
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;//刚开始每个数自成一个集合,每个数自己为根节点
while(m--)
{
char op;
int a,b;
cin>>op>>a>>b;
if(op=='M') p[find(a)]=find(b);//集合合并操作(把a合并到b集合)
else{
if(find(a)==find(b)) cout<<"Yes"<<endl;//如果根节点一样,就输出yes
else cout<<"No"<<endl;
}
}
return 0;
}
2.连通块中点的数量
#include <iostream>
using namespace std;
const int N = 100010;
//只有根节点的s能代表集合的节点数量,所以s中括号里一般都要有find函数先找出根节点
int p[N],s[N];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
int n,m;
cin>>n>>m;
for(int i = 1; i <= n;i++){
p[i] = i;
s[i] = 1;
}
while(m--){
string op;
int a,b;
cin>>op;
if(op == "C"){
cin>>a>>b;
if(find(a) == find(b)) continue;
s[find(b)]+=s[find(a)];//先加size
p[find(a)] = find(b);//再连
}
else if(op == "Q1"){
cin>>a>>b;
if(find(a) == find(b)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
else{
cin>>a;
cout<<s[find(a)]<<endl;
}
}
return 0;
}
3.食物链
#include <iostream>
using namespace std;
const int N = 50010;
//d[x]表示x到父节点的距离,p[x]表示x的父节点
int p[N],d[N];
/*int find(int x)函数两个作作用:
1.使根节点成为x的父节点,并返回x的根节点
2.将d[x]即x到父节点的距离变为x到根节点的距离*/
int find(int x){
if(p[x]!=x){
int t = find(p[x]);
//find完之后,p[x]直接连接根节点了,d[p[x]]是x的父节点p[x]到根节点的距离,即:
/*x到根节点的距离 = x到父节点的距离 + 父节点到根节点的距离
d[x] = d[x] + d[p[x]];*/
//如果不先find那么d[p[x]]就是x的父节点p[x]到父节点的距离,就不一定是p[x]到根节点的距离,即:
/*x到根节点的距离 = x到父节点的距离 + 父节点到父节点的距离
d[x] = d[x] + d[p[x]];*/
//因为find完之后p[x]的子节点就是x,父节点就是根节点0
d[x] += d[p[x]];//这步进行完d[x]是没错的了,就是x到根节点的总距离
//这步再把x的父节点改为根节点,取代之前x和根节点之间的父节点,这样x和根节点就是距离也没错也是直接连接的了
p[x] = t;
}
return p[x];
}
int main(){
int n,m;
cin>>n>>m;
for(int i = 1; i<= n; i++) p[i] = i;
int res = 0;
while(m--){
int t,x,y;
cin>>t>>x>>y;
//1.X或Y比N大,假话
if( x>n || y>n) res++;
else{
//为什么px一定要等于py也就是x和y为什么一定要在一个集合内呢
//如果不在同一个集合内,就无法正确地模拟食物链的关系。因为在同一个集合内,才能通过距离数组 d 来维护元素之间的食物链关系,并查集核心思想也是要在一个集合内
int px = find(x),py = find(y);
if(t == 1){//此时t == 1,表示已经告诉我们x和y是同类
//在一个树上并且x和y不是同类即(d[x] - d[y])%3余1或余2,假话+1
if(px == py && (d[x] - d[y])%3) res++;//注意此时已经经过了find函数所以d[x]是x到根节点的距离
else if(px != py){//当x和y不在一个集合内时把他们合并,注意if条件不要漏,不要直接else,是只有x和y不在一个集合时才要合并
p[px] = py;
d[px] = d[y] - d[x];//1式
}
}
else{//此时告诉我们x吃y
//在一个树上并且x不吃y即(d[x] - d[y] - 1)%3不等于0,假话+1
if(px == py && (d[x] - d[y] - 1)%3) res++;
else if(px != py){//当x和y不在一个集合内时把他们合并,注意if条件不要漏,不要直接else,是只有x和y不在一个集合时才要合并
p[px] = py;
d[px] = d[y] +1-d[x];//2式,1式和2式具体思路看图
}
}
}
}
cout<<res<<endl;
return 0;
}
//简洁版
#include <iostream>
using namespace std;
const int N = 50010;
int p[N],d[N];
int find(int x){
if(p[x] != x){
int t= find(p[x]);
d[x]+=d[p[x]];
p[x] = t;
}
return p[x];
}
int main(){
int n,m;
cin>>n>>m;
for(int i = 1; i <= n ; i++) p[i] = i;
int res = 0;
while(m--){
int op,x,y;
cin>>op>>x>>y;
if(x > n ||y > n) res++;
else{
int px = find(x),py = find(y);
if(op == 1){
if( px == py && (d[x]-d[y])%3) res++;
else if(px != py){
p[px] = py;
d[px] = d[y]-d[x];
}
}
else{
if( px == py && (d[x]-d[y]-1)%3) res++;
else if(px != py){
p[px] = py;
d[px] = d[y]-d[x] + 1;
}
}
}
}
cout<<res<<endl;
return 0;
}
堆
//h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
//ph存储堆中的下标,ph是插入顺序数组
//hp存储元素的插入顺序,h存储的是元素的值,h和hp都是堆数组
//se表示size,插入点在堆中的下标,因为size是关键字所以用se代替
int h[N], ph[N], hp[N], se;
void heap_swap(int a, int b){
//下面三个交换只要保证 swap(hp[a], hp[b])在swap(ph[hp[a]], ph[hp[b]])后面就行,其他无所谓
swap(ph[hp[a]], ph[hp[b]]);//交换堆中的下标
/*上面这一步是交换堆中的下标,为什么不能直接交换a和b:首先,是因为交换a和b会影响后面两个交换。
但是这么交换的实际意思是:只是交换了ph数组中对应a和b的那个值,所以这样既正确修改了映射,又没影响到a和b*/
swap(hp[a], hp[b]);//交换插入顺序
swap(h[a], h[b]);//交换值
}
void down(int r){
int t=r;
if (r * 2 <= se && h[r * 2]< h[t]) t = r * 2;
if (r *2 + 1 <= se && h[r * 2 + 1]< h[t]) t = r * 2 + 1;
if (r != t){
heap_swap(r, t);
down(t);
}
}
void up(int u){
//只要保证比根结点的值大就行了
while (u / 2 && h[u] < h[u / 2]){//此节点索引为u,根节点为u/2
heap_swap(u /2, u);
u /= 2;//下次循环再跟根节点的根节点比...以此类推
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
1.堆排序
//小顶堆:1.根节点小于等于两个子节点;2.假如根节点的索引为x,则左子节点索引为2x,右为2x+1;3.堆在几何意义上是一颗完全二叉树
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int h[N], se;//se是数组长度size
int n, m;
void down(int r)
{
int t = r;
//一定要用h[t]比较,如上图
if (2 * r <= se && h[2 * r] < h[t])//先跟左子节点比
t = 2 * r;
if (2 * r + 1 <= se && h[2 * r + 1] < h[t])//再跟右子节点比
t = 2 * r + 1;
if (r != t)
{
swap(h[r], h[t]);//跟最后比出来的最小的那个结点换值
/*为什么要在if (r != t)里面down(t),而不是这个函数的最后或其他位置:因为当r == t时,根节点就是最小值,也就是之前t没被赋索引;如果t被赋索引了,此时t是两个子节点中最小的那一个,因为这个最小的值被这个小子树的根节点换走了,所以此时这个子节点的值就是根结点的值,是比原来的值更大的,所以以这个子节点为根节点的子树可能就不满足小顶堆了,所以要再down一下,同理,到了以这个子节点为根节点的小子树如果也是这种情况也要继续再down,以此类推。*/
down(t);
}
}
int main()
{
cin >> n >> m;
se = n;
for (int i = 1; i <= n; i++) cin>>h[i];
//使数组成为堆,即初始化堆
for (int i = n / 2; i > 0; i--) down(i);
while (m--)
{
cout << h[1] << " ";
h[1] = h[se--];
down(1);
}
return 0;
}
2.模拟堆
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
//ph存储堆中的下标,ph是插入顺序数组
//hp存储元素的插入顺序,h存储的是元素的值,h和hp都是堆数组
//se表示size,插入点在堆中的下标
int h[N], ph[N], hp[N], se;
//参数是堆中的下标
void heap_swap(int a, int b){
swap(ph[hp[a]], ph[hp[b]]);//交换堆中的下标
/*上面这一步是交换堆中的下标,为什么不能直接交换a和b:首先,是因为交换a和b会影响后面两个交换。
但是这么交换的实际意思是:只是交换了ph数组中对应a和b的那个值,所以这样既正确修改了映射,又没影响到a和b*/
swap(hp[a], hp[b]);//交换插入顺序
swap(h[a], h[b]);//交换值
}
void down(int r){
int t=r;
if (r * 2 <= se && h[r * 2]< h[t]) t = r * 2;
if (r *2 + 1 <= se && h[r * 2 + 1]< h[t]) t = r * 2 + 1;
if (r != t){
heap_swap(r, t);
down(t);
}
}
void up(int u){
//只要保证比根结点的值大就行了
while (u / 2 && h[u] < h[u / 2]){//此节点索引为u,根节点为u/2
heap_swap(u /2, u);
u /= 2;//下次循环再跟根节点的根节点比...以此类推
}
}
int main(){
int m;
cin>>m;
int n = 0;//n表示插入的第几个数
while(m--)
{
string op;
int k,x;
cin>>op;
//1.插入一个数x
if(op=="I")
{
cin>>x;
n++;
se++;//se表示在堆中的下标
h[se]=x;
hp[se]=n;
ph[hp[se]]=se;
up(se);//新插入的值浮上去,注意这点易忘
}
//2.输出当前集合中的最小值
else if(op=="PM") cout<<h[1]<<endl;
//3.删除当前集合中的最小值
else if(op=="DM")
{
heap_swap(1,se);//删除操作都是通过交换实现,删除完要进行沉或者浮
se--;
down(1);
}
//4.删除第k个插入的数
else if(op=="D")
{
cin>>k;
int u = ph[k];
heap_swap(u,se);//删除操作都是通过交换实现,删除完要进行沉或者浮
se--;
up(u);
down(u);
}
//5.修改第 k个插入的数,将其变为 x
else if(op=="C")
{
//ph[k]表示第k个数在堆中的下标
cin>>k>>x;
h[ph[k]]=x;
down(ph[k]);
up(ph[k]);
}
}
return 0;
}
cin>>k;
int u = ph[k];
heap_swap(u,se);
se--;
up(u);
down(u);
//这里不能用直接用ph[K],ph数组在heap_swap里有操作,heap_swap之后ph[k]会改变,而如果用另一个变量先ph[k]的值就不会
cin>>k;
heap_swap(ph[k],se);
se--;
up(ph[k]);
down(ph[k]);
哈希表
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i - 1];
p[i] = p[i - 1] * P;
}
// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
1.模拟散列表
//拉链法,其实本质上就是N个单链表,就是头结点head变成了h[i]
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003; // 取大于1e5的第一个质数,取质数冲突的概率最小 可以百度
//h是一个数组,对应索引上面连着一个链表,h[i]的值是i索引上的链表中的头结点的下一个元素的指针,头结点是留空的;
int h[N], e[N], ne[N], idx;
//插入的方式是往头结点插入,具体可以看单链表
void insert(int x) {
int k = (x % N + N) % N;//防止x是负数
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool 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;
}
int main() {
int n;
cin >> n;
memset(h, -1, sizeof h); //将h数组全部元素的值赋为-1
while (n--) {
string op;
int x;
cin >> op >> x;
if (op == "I") insert(x);
else {
if (find(x)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
return 0;
}
//开放寻址法
#include <cstring>
#include <iostream>
using namespace std;
//开放寻址法一般开 数据范围的 2~3倍, 这样大概率就没有冲突了
const int N = 200003; //大于数据范围的第一个质数
const int null = 0x3f3f3f3f; //规定空指针为 null 0x3f3f3f3f
int h[N];
//插入与查找合为一体的函数
int find(int x) {
int k = (x % N + N) % N;
//h[k] != null是插入操作的条件,h[k] != x是查找操作的条件
//跳出循环的条件h[k] == null既是插入位置的最终结果,也是未找到待查找元素的最终结果
//这里可能会考虑用for循环代码更短,但提交可能会超时,采用while循环会更快
while ( h[k] != null && h[k] != x ) {
k++;
if (k == N) k = 0;
}
return k;
}
int main() {
int n;
cin >> n;
memset(h, 0x3f, sizeof h); //规定空指针为 0x3f3f3f3f
while (n--) {
string op;
int x;
cin >> op >> x;
if (op == "I") {
h[find(x)] = x;
} else {
if (h[find(x)] == x) {
puts("Yes");
} else {
puts("No");
}
}
}
return 0;
}
2.字符串哈希
#include <iostream>
using namespace std;
const int N = 100010,P = 131;//P=131 或 P=13331
typedef unsigned long long ull;//因为Q是2的64次方
char str[N];
//p[i]是131的i次方,主要就是用到一个p[r - l + 1];h[i]是指前i个字母的哈希值,p和h下标都从1开始考虑
ull h[N],p[N];
ull get(int l,int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
int main(){
int n,m;
cin>>n>>m>>str;
p[0] = 1;
//初始化
for(int i = 1; i <= n; i++){
p[i] = p[i-1] * P;
h[i] = h[i-1] * P + str[i - 1];//计算字符串哈希值的公式
}
while(m--){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(get(l1,r1) == get(l2,r2)) puts("Yes");
else puts("No");
}
return 0;
}
C++ STL简介
vector, 变长数组,倍增的思想
size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end()
[]
支持比较运算,按字典序
pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
string,字符串
size()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址
queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素
priority_queue, 优先队列,默认是大根堆
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素
deque, 双端队列
size()
empty()
clear()
front()/back()
push_back()/pop_back()
push_front()/pop_front()
begin()/end()
[]
set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
size()
empty()
clear()
begin()/end()
++, -- 返回前驱和后继,时间复杂度 O(logn)
set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--
bitset, 圧位
bitset<10000> s;
~, &, |, ^
>>, <<
==, !=
[]
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反
第三讲 搜索与图论
DFS
1.排列数字
#include<iostream>
using namespace std;
const int N = 10;
int path[N];//保存序列
bool st[N];//数字是否被用过,标记数组
int n;
void dfs(int u)
{
if(u > n)//数字填完了,输出
{
for(int i = 1; i <= n; i++)//输出方案
cout << path[i] << " ";
cout << endl;
}
else{
for(int i = 1; i <= n; i++)//空位上可以选择的数字为:1 ~ n
{
if(!st[i])//如果数字 i 没有被用过
{
path[u] = i;//放入空位
st[i] = true;//数字被用,修改状态
dfs(u + 1);//填下一个位
//回溯:原路返回,把原来满足条件的位置的状态修改回来,并把值置为默认值
st[i] = false;
path[u] = 0;//这里可要可不要,因为后面又满足条件赋值时会覆盖
}
}
}
}
int main()
{
cin >> n;
dfs(1);
}
2.n-皇后问题
#include <iostream>
using namespace std;
const int N = 20;
// bool数组用来判断搜索的下一个位置是否可行
// col列,dg对角线,udg反对角线
// g[N][N]用来存路径
int n;
char g[N][N];
bool col[N], dg[N], udg[N];
//整体按行搜索
void dfs(int y) {
// y从0开始,y == n 表示已经搜了n行,故输出这个方案
if (y == n) {
for (int i = 0; i < n; i ++ ) puts(g[i]);//按行输出
puts("");
return;
}
//局部按列搜索
for (int x = 0; x < n; x ++ )
// 剪枝(对于不满足要求的点,不再继续往下搜索)
// dg[y - x + n],+n是为了保证下标非负
if (!col[x] && !dg[y - x + n] && !udg[y + x]) {
g[x][y] = 'Q';
col[x] = dg[y - x + n] = udg[y + x] = true;
dfs(y + 1);//继续下一行
//回溯:产生了一种方案后原路返回,把原来满足条件的位置的状态修改回来,并把值置为默认值
col[x] = dg[y - x + n] = udg[y + x] = 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);
return 0;
}
BFS
1.走迷宫
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 110;
typedef pair<int, int> PII;
int n, m;
int g[N][N], d[N][N];//g是迷宫数组,d是离原点的距离数组
int bfs()
{
//定义并初始化
queue< pair<int, int> > q;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//顺时针,上右下左
q.push({0, 0});//先将原点入队
//遍历队列
while (q.size())
{
PII t = q.front();//取队头元素
q.pop();//取完就要出队
for (int i = 0; i < 4; i++)//遍历队头四个方向
{
int x = t.first + dx[i], y = t.second + dy[i];
//g[x][y] == 0表示能走的点,d[x][y] == -1表示如果当前状态是第一次遍历才记录距离,入队
//d[x][y] == -1很关键,在此走迷宫问题中,最先到d[n - 1][m -1]的肯定是最短距离,但是此时队列中可能还会剩几个元素,所以while循环还会继续,还会继续遍历剩下的点,但是不用担心d[n - 1][m -1]被覆盖,因为d[n - 1][m -1]==-1时不会进入if,所以while循环结束后d[n - 1][m -1]即是最小值,或者直接加一个if(x == n - 1 && y == m - 1) return d[n - 1][m -1];更好理解
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,所以+1
if(x == n - 1 && y == m - 1) return d[n - 1][m -1];//达到最终状态退出
q.push({x, y});//将新坐标入队
}
}
}
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];
memset(d, -1, sizeof(d));//将d数组各元素初始值置为-1
d[0][0] = 0;//{0,0}离原点的距离为0,也表示已走过,即从原点开始
cout << bfs() << endl;
return 0;
}
2.八数码
#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
int bfs(string start)
{
//定义并初始化
string end = "12345678x";//定义目标状态
queue<string> q;
unordered_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();//取队头
q.pop();
//记录当前状态的距离,如果是最终状态则返回距离
int dist = d[t];
if(t == end) return d[t];//达到最终状态退出
int k = t.find('x');//查询x在字符串中的下标
int x = k / 3, y = k % 3;//将字符串下标转换为坐标系坐标
for(int i = 0; i < 4; i++)//遍历队头的四个方向
{
int a = x + dx[i], b = y + dy[i];//移动一次后的坐标
if(a >= 0 && a < 3 && b >= 0 && b < 3)
{
swap(t[k], t[a * 3 + b]);//转移x,即交换x与对应字符,a * 3 + b是坐标转换为下标的公式
//如果当前状态是第一次遍历,才记录距离,入队
if(!d.count(t))
{
d[t] = dist + 1;
q.push(t);
}
//四个方向可能有多个方向满足条件,所以要还原状态,所以同一个初始状态遍历完四个方向后因为q.push(t),q里面可能会存有多个元素
swap(t[k], t[a * 3 + b]);
}
}
}
//无法转换到目标状态,返回-1
return -1;
}
int main()
{
char c;
string start;
//输入起始状态
for(int i = 0; i < 9; i++)
{
cin >> c;
start += c;
}
cout << bfs(start) << endl;
return 0;
}
树与图的深度优先遍历
树与图的遍历,时间复杂度O(n+m),n表示点数,m表示边数
int dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
树的重心
//邻接表是一种常用的图存储结构,其原理基于将图的每个顶点表示为一个链表,链表中存储与该顶点相邻的其他顶点。
//树也是特殊的图
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
const int M = 2 * N;//以有向图的格式存储无向图,所以每个节点至多对应2n-2条边
int h[N];
int e[M];
int ne[M];
int idx;
int n;
int ans = N;//初始化为n是因为要和res比较,挑出最小的连通块中点数的最大值
bool st[N];
//h[a]即a为顶点的链表的头指针
//add是在以a为顶点的链表插入元素b
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
//参数u是题中所说的结点的编号同时也是结点的值,dfs函数是找到u的所有子树的点数加上自己的一个,即sum
int dfs(int u) {
int res = 0;
st[u] = true;
int sum = 1;//u自己算一个
//遍历以h[u]为头指针的链表,手动模拟过程如上图
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
//如果没被访问过
if (!st[j]) {
int s = dfs(j);
res = max(res, s);
sum += s;
}
}
//sum此时是所有子树的点数加上自己的一个
res = max(res, n - sum);
ans = min(res, ans);
return sum;
}
int main() {
memset(h, -1, sizeof h);//各个链表的头指针
cin >> n;//n个节点
//n-1条边
for (int i = 0; i < n - 1; i++) {
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
dfs(1);//此题当作双向图来存储,所以各结点之间都必有通路,所以从1~n任何一个结点开始都行
cout << ans << endl;
return 0;
}
树与图的广度优先遍历
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);
}
}
}
图中点的层次
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int n,m;
int h[N],e[N],ne[N],d[N];
int idx;
void add(int a,int b){
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
int bfs(){
queue<int> q;
memset(d, -1 , sizeof d);//注意初始化的位置
q.push(1);
d[1] = 0;
while(q.size()){
int t = q.front();
q.pop();
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(d[j] == -1){
d[j] = d[t] + 1;
if(j == n) return d[j];
q.push(j);
}
}
}
return d[n];
}
int main(){
cin>>n>>m;
int a,b;
memset(h, -1 ,sizeof h);//要在add(a,b)前初始化
while(m--){
cin>>a>>b;
add(a,b);
}
cout<<bfs()<<endl;
return 0;
}
拓扑排序
拓扑排序,时间复杂度O(n+m),n表示点数,m表示边数
bool topsort()
{
int hh = 0, tt = -1;
// d[i] 存储点i的入度
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (-- d[j] == 0)
q[ ++ tt] = j;
}
}
// 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
return tt == n - 1;
}
有向图的拓扑序列
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int h[N], e[N], ne[N], idx;//邻接表
int q[N], hh = 0, tt = -1;//数组模拟队列
int n, m;
int d[N];//存储点的入度数
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void topsort()
{
//将所有入度为0的点入队
for (int i = 1; i <= n; i++)
{
if (d[i] == 0) q[++tt] = i;
}
while (tt >= hh)
{
int t = q[hh++];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
//如果此点入度-1后为0则入队
if (--d[j] == 0) q[++tt] = j;
}
}
if (tt == n - 1)
{
for (int i = 0; i < n; i++) cout << q[i] << " " ;
}
else cout << -1 << endl;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b);
d[b]++;//add(a, b)说明有到b的边,b的入度+1
}
topsort();
return 0;
}
Dijkstra
1.Dijkstra求最短路 I
//朴素Dijkstra算法,时间复杂度O(n*n+m),n表示点数,m表示边数
int g[N][N]; // 存储每条边
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否已经确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i ++ )
{
int t = -1; // 在还未确定最短路的点中,寻找距离最小的点
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 用t更新其他点的距离
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
st[t] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 510;
int g[N][N];//稠密图用邻接矩阵
/*
eg.
1≤n≤500,
1≤m≤10^5,m近似于n^2,为稠密图,用邻接矩阵
1≤n,m≤1.5×10^5,m近似于n,为稀疏图,用邻接表*/
int d[N];//各个点到1号点的距离
bool st[N];//用于标记该点是否已经确定最小距离
int n, m;
int Dijkstra()
{
memset(d, 0x3f3f3f3f, sizeof d);//到原点的距离数组赋值无限大,赋值时赋0x3f和0x3f3f3f3f是一样的
d[1] = 0;
//n个点遍历n次
for(int i=0; i<n; i++) {
int t = -1;
/*在最外面for循环的第一遍时,因为除了d[1] = 0外,各个点到1点的距离d[i]都还是无穷大
所以这下面这个小for循环只执行一次,使t=1*/
//找到没有确定最短路径的节点中距离原点最近的点t,用于之后那个小for循环的d[t] + g[t][j],用于更新各个点的d[i]
for(int j=1; j<=n; j++)
if(!st[j] && (t == -1 || d[t] > d[j]))
t = j;
st[t] = true;//修改状态
/*在最外面for循环的第一遍时下面这个小for循环是把所有点的d[i]更新为d[i]或者g[1][i],也就是1到各点的边长
如果g[1][i]不存在则说明1号点没有到该点的边,d[i]还是无穷大*/
//此for循环用于判断 当前点到1点的距离加上当前点到每个点的距离 和 每个点到1点的距离 哪个更小使各个点的d[i]取更小值
for(int j=1; j<=n; j++)
d[j] = min(d[j], d[t] + g[t][j]);
}
if(d[n] == 0x3f3f3f3f) return -1;//没路
return d[n];//只要有值就是最短距离
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);//邻接矩阵赋无穷大 ,赋值时赋0x3f和0x3f3f3f3f是一样的
while(m--) {
int x, y, z;
cin >> x >> y >> z;
g[x][y] = min(g[x][y], z);//防止重边,保留更小的距离
}
cout << Dijkstra() << endl;
return 0;
}
2.Dijkstra求最短路 II
//堆优化版Dijkstra算法,时间复杂度O(mlogn),n表示点数,m表示边数
typedef pair<int, int> PII;
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
bool st[N]; // 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // first存储距离,second存储节点编号
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] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 150010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;//w[N]是权重数组
int d[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 ++ ;
}
int dijkstra()
{
memset(d, 0x3f, sizeof d);
d[1] = 0;
//创建了一个最小堆,其中元素是类型为PII的对,按照递减的顺序(最小元素位于队列前面)存储
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1});//first是距离,second是点号,不能换,要根据距离排序
while (heap.size())
{
PII t = heap.top();
heap.pop();
int p = t.second;
if (st[p]) continue;
st[p] = true;
for (int i = h[p]; i != -1; i = ne[i])
{
int j = e[i];
if (d[j] > d[p] + w[i])
{
d[j] = d[p] + w[i];
heap.push({d[j], j});
}
}
}
if (d[n] == 0x3f3f3f3f) return -1;
return d[n];
}
int main()
{
cin>>n>>m;
int a, b, c;
memset(h, -1, sizeof h);
while (m -- )
{
cin>>a>>b>>c;
add(a, b, c);
}
cout<<dijkstra()<<endl;
return 0;
}
bellman-ford
有边数限制的最短路
为什么是dist[n]>0x3f3f3f3f/2, 而不是dist[n] == 0x3f3f3f3f
为什么需要last[a]数组
//bellman_ford,时间复杂度O(nm),n表示点数,m表示边数
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
struct Edge // 边,a表示出点,b表示入点,w表示边的权重
{
int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i = 0; i < n; i ++ )
{
for (int j = 0; j < m; j ++ )
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
if (dist[b] > dist[a] + w)
dist[b] = dist[a] + w;
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge
{
int a, b, w;
}edges[M];
int n, m, k;
int d[N];
int last[N];
void bellman_ford()
{
memset(d, 0x3f, sizeof d);
d[1] = 0;
//遍历最大限制边数
for (int i = 0; i < k; i ++ )
{
memcpy(last, d, sizeof d);//备份
//遍历总边数
for (int j = 0; j < m; j ++ )
{
Edge e = edges[j];
d[e.b] = min(d[e.b], last[e.a] + e.w);
}
}
}
int main()
{
cin>>n>>m>>k;
for (int i = 0; i < m; i ++ )
{
int a, b, w;
cin>>a>>b>>w;
edges[i] = {a, b, w};
}
bellman_ford();
if (d[n] > 0x3f3f3f3f / 2) puts("impossible");
else cout<<d[n]<<endl;
return 0;
}
spfa
1.spfa求最短路
//时间复杂度平均情况下O(m),最坏O(nm),n表示点数,m表示边数
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j]) // 如果队列中已存在j,则不需要将j重复插入
{
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
//SPFA算法是对bellman-ford算法的一个优化。
/*Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;
但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,
因此假如有负权回路请你不要用SPFA否则会死循环。*/
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int d[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 ++ ;
}
int spfa()
{
memset(d, 0x3f, sizeof d);
d[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
int t = q.front();
q.pop();
/* Dijkstra算法中的st数组保存的是当前确定了到源点距离最小的点,
且一旦确定了最小那么就不可逆了(不可标记为true后改变为false);
SPFA算法中的st数组仅仅只是表示的当前发生过更新的点,
且spfa中的st数组可逆(可以在标记为true之后又标记为false)。*/
st[t] = false;//从队列中取出来之后该节点st被标记为false,代表之后该节点如果发生更新可再次入队
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (d[j] > d[t] + w[i])
{
d[j] = d[t] + w[i];
if (!st[j])//如果当前点已经在数列中,就更新一下数值而不用加入到队列当中
{
q.push(j);
st[j] = true;
}
}
}
}
return d[n];
}
int main()
{
cin>>n>>m;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
cin>>a>>b>>c;
add(a, b, c);
}
/*Bellman_ford算法里最后return-1的判断条件写的是dist[n]>0x3f3f3f3f/2;
而spfa算法写的是dist[n]==0x3f3f3f3f;其原因在于Bellman_ford算法会遍历所有的边,
因此不管是不是和源点连通的边它都会得到更新;但是SPFA算法不一样,它相当于采用了BFS,
因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。*/
if (spfa() == 0x3f3f3f3f) puts("impossible");
else cout<<d[n]<<endl;;
return 0;
}
2.spfa判断负环
//时间复杂度O(nm),n表示点数,m表示边数
int n, m;
int h[N], w[M], e[M], ne[M], idx; // 邻接表存储所有边
int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N]; // 存储每个点是否在队列中
// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
// 不需要初始化dist数组
// 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
q.push(i);
st[i] = true;
}
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
bool st[N];
int d[N];
int cnt[N]; //cnt[x] 表示 当前从1-x的最短路的边数
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
bool spfa(){
// 这里不需要初始化d数组为正无穷的原因是,如果存在负环,那么dist不管初始化为多少,都会被更新
queue<int> q;
//不仅仅是1了,因为点1可能到不了有负环的点, 因此把所有点都入队
for(int i=1;i<=n;i++){
q.push(i);
st[i]=true;
}
while(q.size()){
int t = q.front();
q.pop();
st[t]=false;
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(d[j] > d[t] + w[i]){
d[j] = d[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!st[j]){
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while(m--){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
if (spfa()) puts("Yes");
else puts("No");
return 0;
}
Floyd
Floyd求最短路
//时间复杂度O(n*n*n)
//初始化
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;
// 算法结束后,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]);
}
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;//INF表示正无穷,当算法要用memset函数时用0x3f3f3f3f表示无穷大
int n, m, q;
int d[N][N];
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() {
cin >> n >> m >> q;
//初始化邻接矩阵
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;//
int a,b,c;
while(m--) {
cin >> a >> b >> c;
d[a][b] = min(d[a][b], c);//注意这步min的参数
}
floyd();
while(q--) {
cin >> a >> b;
//由于有负权边存在所以约大过INF/2
if(d[a][b] > INF/2) puts("impossible");
else cout << d[a][b] << endl;
}
return 0;
}
Prim
Prim算法求最小生成树
//朴素版prim算法,时间复杂度是O(n*n+m),n表示点数,m表示边数
int n; // n表示点数
int g[N][N]; // 邻接矩阵,存储所有边
int dist[N]; // 存储其他点到当前最小生成树的距离
bool st[N]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
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]))
t = j;
if (i && dist[t] == INF) return INF;
if (i) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m;
int g[N][N];
int d[N];//某点离集合的距离
bool st[N];//标记是否已加入集合
int prim()
{
memset(d, 0x3f, sizeof d);
d[1] = 0;
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 || d[t] > d[j]))
t = j;
//如果d[t] == INF 就说明当前遍历的点离集合没有通路 即不可能生成最小生成树,返回INF
if (d[t] == INF) return INF;
st[t] = true;//加入集合
//更新总边权
res += d[t];
/*这里是g[t][j]而不是d[t]+g[t][j]是因为:
要更新的是各点到集合的最小距离即到最后加入集合的那个点即当前遍历的点的距离,而不是原点*/
for (int j = 1; j <= n; j ++ ) d[j] = min(d[j], g[t][j]);
}
return res;
}
int main()
{
cin>>n>>m;
memset(g, 0x3f, sizeof g);
while (m -- )
{
int a, b, c;
cin>>a>>b>>c;
g[a][b] = g[b][a] = min(g[a][b], c);//无向图是特殊的有向图,赋两条边
}
int t = prim();
if (t == INF) puts("impossible");
else cout<<t<<endl;
return 0;
}
Kruskal
Kruskal算法求最小生成树
//时间复杂度是O(mlogm),m表示边数
int n, m;
int p[N];
int res;//集合中的总边权
int cnt;//集合中的边数
struct Edge
{
int a, b, w;
bool operator < (const Edge &e) const
{
return w < e.w;
}
}edges[M];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void kruskal()
{
//按升序给每条边排序
sort(edges, edges + m);
// 初始化并查集
for (int i = 1; i <= n; i ++ ) p[i] = i;
//遍历所有边
for (int i = 0; i < m; i ++ )
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
int pa = find(a), pb = find(b);
if (pa != pb)
{
p[pa] = pb;
res += w;
cnt ++ ;
}
}
}
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 200010;
int n, m;
int p[N];
int res;//集合中的总边权
int cnt;//集合中的边数
struct Edge
{
int a, b, w;
/*
一般形式:
bool operator<(const ClassName &other) const {
// 在这里编写比较逻辑,返回当前对象是否小于另一个对象
}
*/
//“<”运算符的重载函数,它定义了对 Edge 对象之间按照 w 成员变量的大小进行比较。
bool operator < (const Edge &e) const
{
return w < e.w;//w 和 W.w 分别表示当前对象和另一个对象的 w 成员变量的值。
}
}edges[M];
//找根结点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void kruskal()
{
//先按升序给每条边排序
sort(edges, edges + m);
// 初始化并查集
for (int i = 1; i <= n; i ++ ) p[i] = i;
//遍历所有边(较小权重的边先连,各个点连好之后就都在同一个集合了,再有边也加入不了了)
for (int i = 0; i < m; i ++ )
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
int pa = find(a), pb = find(b);
if (pa != pb)
{
p[pa] = pb;
res += w;
cnt ++ ;
}
}
}
int main()
{
cin >> n >> m;
for (int i = 0; i < m; i ++ )
{
int a, b, w;
cin >> a >> b >> w;
edges[i] = {a, b, w};
}
kruskal();
if (cnt < n - 1) cout << "impossible" << endl;//最小生成树最少要有n-1条边,注意不要用m
else cout << res << endl;
return 0;
}
染色法判定二分图
染色法判定二分图
//时间复杂度O(n+m),n为点数,m为边数
//二分图:直接连接的点不在同一个集合
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 200010;//有向图模拟无向图,边数设成点数的两倍
int n, m;
int h[N], e[M], ne[M], idx;//有向图模拟无向图,用e[M], ne[M]而不是N
int color[N];//保存各个点的颜色,0 未染色,1 是一种颜色,2 是另一种颜色
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
//u 是点,c 是颜色,给该点及其连通子块中的点染色
bool dfs(int u, int c)
{
//先把 u 点染为 c 色
color[u] = c;
//再遍历 u 的邻接表,将 u 的连通子块中的点全部染色
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
//如果当前点未染色,再给遍历的当前点及其连通子块中的点染色
if (!color[j])
{
if (!dfs(j, 3 - c)) return false;
}
//如果已染色,并且颜色和该点一样
else if (color[j] == c) return false;
}
return true;
}
int main()
{
cin>>n>>m;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
cin>>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))//这里 1 和 2 都行
{
flag = false;
break;
}
}
if (flag) puts("Yes");
else puts("No");
return 0;
}
匈牙利算法
二分图的最大匹配
//时间复杂度是O(nm),n是点数,m是边数
//二分图是指图中所有点只分两个集合
//匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点即点是一对一的。
//二分图中一个点可能会连接多个点,但并不能称为匹配,当这个点确定了对应另一个集合中的哪个点时,匹配数加一
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;
int g[N][N];//稠密图采用邻接矩阵
int match[N];//match[i]表示A集合的点i目前匹配的B集合中的点
bool st[N];//st[i]表示,B集合中的点i是否已经匹配
int res;//最大匹配数
//使 x 点在这一轮中未遍历的 B 集合的点中匹配一个点,匹配成功返回 true
bool find(int x)
{
//遍历 A 集合中的 x 对应 B 集合中的各个点
for(int i = x, j = 1; j <= n2; j++){
//如果i到j有边
if(g[i][j]){
//该点没有被匹配
if (!st[j])
{
//虽然每轮B集合中的各点的匹配状态都要刷新,但是也必须要有st[j]来确定状态,以便于find(match[j])时排除此点去找x连接的其他点
st[j] = true;//修改状态,注意位置,要在find(match[j])之前即if之前
/*
如果 j 点没有匹配 A 集合中的任意一点即match[j] == 0,
或者 j 点目前匹配的 A 集合中的点还有其他相连接的点可匹配即 find(match[j])
*/
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
}
return false;
}
int main()
{
cin>>n1>>n2>>m;
while (m -- )
{
int a, b;
cin>>a>>b;
g[a][b] = 1;//有边时权重为1
}
//遍历A集合中的各点
for (int i = 1; i <= n1; i ++ )
{
//因为可以使 B 集合中的已匹配的点可以“被抢 ”(find(match[j])),所以每轮都要刷新使 B 集合中的各点都可以访问
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
cout<<res<<endl;
return 0;
}
第四讲 数学知识
质数
1.试除法判定质数
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
return false;
return true;
}
#include <iostream>
#include <algorithm>
using namespace std;
bool is_prime(int x)
{
if (x < 2) return false;
// x 的一个更小的因子为 i ,另一个更大的因子为 x / i ,遍历所有更小的因子
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
return false;
return true;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
if (is_prime(x)) puts("Yes");
else puts("No");
}
return 0;
}
2.分解质因数
#include <iostream>
#include <algorithm>
using namespace std;
void divide(int x)
{
/*这里遍历的 i 肯定所有都是质数,假如 i 是合数那么 i 肯定也可分解为更小的质数相乘,由于循环是从小到大遍历的
所以 i 分解的更小的质数肯定是遍历过, x 除过了的,所以不可能遍历到合数*/
//因最多只有一个大于 sqrt(n) 的质因子,所以先遍历小于 sqrt(n) 的
for (int i = 2; i <= x / i; i ++ )
//每有一个质因子
if (x % i == 0)
{
int s = 0;
while (x % i == 0) x /= i, s ++ ;
cout << i << ' ' << s << endl;
}
//如果最后 x > 1 ,那此时的 x 就是那个大于 sqrt(n) 的质因子
if (x > 1) cout << x << ' ' << 1 << endl;
cout << endl;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
divide(x);
}
return 0;
}
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
int s = 0;
while (x % i == 0) x /= i, s ++ ;
cout << i << ' ' << s << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl;
cout << endl;
}
3.筛质数
//埃氏筛法
#include <iostream>
#include <algorithm>
using namespace std;
const int N= 1000010;
int primes[N], cnt;// primes 是存储质数的数组, cnt 是质数的数量
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ ){
if(!st[i]){
primes[cnt ++ ] = i;
//把此质数的所有倍数筛掉
for (int j = i + i; j <= n; j += i) st[j] = true;
}
}
}
int main()
{
int n;
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}
int primes[N], cnt;// primes 是存储质数的数组, cnt 是质数的数量
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ ){
if(!st[i]){
primes[cnt ++ ] = i;
//把此质数的所有倍数筛掉
for (int j = i + i; j <= n; j += i) st[j] = true;
}
}
}
//线性筛法,数据大时会快很多
//详解参考:https://blog.youkuaiyun.com/littlegengjie/article/details/134164936
#include <iostream>
#include <algorithm>
using namespace std;
const int N= 1000010;
int primes[N], cnt;
bool st[N];//st[x]存储x是否被筛掉,非质数要被筛掉,起初全为 false ,全为质数
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
//当 i 是质数
if (!st[i]) primes[cnt ++ ] = i;
//在筛 i 的最小质因子的过程中把 primes[j] * i <= n 的所有合数筛出来
for (int j = 0; primes[j] <= n / i; j ++ )
{
/*
当 i % primes[j] != 0 时:
primes[j]一定小于 i 的最小质因子,而且 primes[j] 一定是primes[j] * i的最小质因子。
把对应 primes[j] 的合数筛掉
*/
st[primes[j] * i] = true;
/*
当 i % primes[j] == 0 时:
就说明枚举到 i 的最小质因子,退出循环
*/
if (i % primes[j] == 0) break;
}
}
}
int main()
{
int n;
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
约数
1.试除法求约数
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> get_divisors(int x)
{
vector<int> res;
//遍历更小的约数就行,更大的约数可以通过 x / i pushback进去
for (int i = 1; i <= x / i; i ++ )
if (x % i == 0)
{
res.push_back(i);
//避免 x = i * i 时重复放入同一个数
if (i != x / i) res.push_back(x / 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 (int x : res) cout << x << ' ';
cout << endl;
}
return 0;
}
vector<int> get_divisors(int x)
{
vector<int> res;
for (int i = 1; i <= x / i; i ++ )
if (x % i == 0)
{
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
sort(res.begin(), res.end());
return res;
}
2.约数个数
//约数个数定理:一个正整数的约数个数等于其质因子分解中每个质数指数加1的乘积。
/*
例如:
将378000分解质因数378000=2^4×3^3×5^3×7^1
由约数个数定理可知378000共有正约数(4+1)×(3+1)×(3+1)×(1+1)=160个。
*/
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;//primes[i] 表示 i 这个质因子对应的指数
while (n -- )
{
int x;
cin >> x;
//分解质因数
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0)
{
x /= i;
primes[i] ++ ;
}
//最多只有一个大于 sqrt(n) 的质因子,要考虑这个大于 sqrt(n) 的质因子
if (x > 1) primes[x] ++ ;
}
LL res = 1;
for (pair<int,int> p : primes) res = res * (p.second + 1) % mod;
cout << res << endl;
return 0;
}
3.约数之和
/*
例:
将 37800 分解质因数可得
360=2^4*3^3*5^3+7^1
1.由约数个数定理可知 378000 共有正约数(4+1)×(3+1)×(3+1)×(1+1)=160个。
2.由约数和定理可知, 378000 所有正约数的和为
(2^0+2^1+2^2+2^3+2^4)×(3^0+3^1+3^2+3^3)×(5^0+5^1+5^2+5^3) x (7^0+7^1)
=(1+2+4+8+16)(1+3+9+27)(1+5+25+125)(1+7)=31×40×156 x8 =1537920
*/
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;
while (n -- )
{
int x;
cin >> x;
//分解质因数
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0)
{
x /= i;
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;
}
LL res = 1;
for (pair<int,int> p : primes)
{
LL a = p.first, b = p.second;
LL t = 1;
while (b -- ) t = (t * a + 1) % mod;//算出开头例子的括号项
res = res * t % mod;//把括号项乘起来
}
cout << res << endl;
return 0;
}
4.最大公约数
//欧几里得算法(辗转相除法)
#include <iostream>
using namespace std;
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;//如果 b 不为0,则返回 gcd(b, a % b),否则返回 a。
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int a, b;
cin>>a>>b;
cout<<gcd(a,b)<<endl;
}
return 0;
}
欧拉函数
1.欧拉函数
//互质是公约数只有1的两个整数,叫做互质整数。
#include <iostream>
using namespace std;
int phi(int x)
{
int res = x;//res初始值是x!!
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
res = res / i * (i - 1);//相当于 N * (1 - 1 / p),是为了防止小数
while (x % i == 0) x /= i;
}
if (x > 1) res = res / x * (x - 1);
return res;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
cout << phi(x) << endl;
}
return 0;
}
int phi(int x)
{
int res = x;//res初始值是x
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
res = res / i * (i - 1);//相当于 N * (1 - 1 / p),是为了防止小数
while (x % i == 0) x /= i;
}
if (x > 1) res = res / x * (x - 1);
return res;
}
2.筛法求欧拉函数
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1000010;
int primes[N], cnt;
int euler[N];//存储元素的欧拉函数
bool st[N];
void get_eulers(int n)
{
// 1 的欧拉函数是 1
euler[1] = 1;
//在线性筛法的过程中求出 1 到 n 的欧拉函数
for (int i = 2; i <= n; i ++ )
{
//当 i 是质数
if (!st[i])
{
primes[cnt ++ ] = i;
//质数的欧拉函数为 i - 1
euler[i] = i - 1;
}
for (int j = 0; primes[j] <= n / i; j ++ )
{
//筛掉对应primes[j]的合数
int t = primes[j] * i;
st[t] = true;
//当 i % primes[j] == 0 时,primes[j] 是 i 的最小质因子,也是 primes[j] * i 的最小质因子
if (i % primes[j] == 0)
{
//修正 primes[j] * i 的欧拉函数
euler[t] = euler[i] * primes[j];
break;
}
//当 i % primes[j] != 0 时,primes[j] 不是 i 的最小质因子,但是 primes[j] * i 的最小质因子
//修正 primes[j] * i 的欧拉函数
euler[t] = euler[i] * (primes[j] - 1);
}
}
}
int main()
{
int n;
cin >> n;
get_eulers(n);
LL res = 0;
for (int i = 1; i <= n; i ++ ) res += euler[i];
cout << res << endl;
return 0;
}
int primes[N], cnt; // primes[]存储所有素数
int euler[N]; // 存储每个数的欧拉函数
bool st[N]; // st[x]存储x是否被筛掉
void get_eulers(int n)
{
euler[1] = 1;
for (int i = 2; i <= n; i ++ )
{
if (!st[i])
{
primes[cnt ++ ] = i;
euler[i] = i - 1;
}
for (int j = 0; primes[j] <= n / i; j ++ )
{
int t = primes[j] * i;
st[t] = true;
if (i % primes[j] == 0)
{
euler[t] = euler[i] * primes[j];
break;
}
euler[t] = euler[i] * (primes[j] - 1);
}
}
}
快速幂
1.快速幂
#include <iostream>
using namespace std;
typedef long long LL;
LL qmi(int a, int b, int p)
{
//防止 p = 1
LL res = 1 % p;
while (b)
{
//如果当前 b 的最低位为 1 则把结果乘上当前底数 a ,为 0 就不用乘
if (b & 1) res = res * a % p;
//每次都要更新底数,使 a 为 a 的平方
a = (LL)a * a % p;//强转其中一个操作数防止溢出
//b右移一位
b >>= 1;
}
return res;
}
int main()
{
int n;
cin>>n;
while (n -- )
{
int a, b, p;
cin>>a>>b>>p;
cout<<qmi(a, b, p)<<endl;;
}
return 0;
}
LL qmi(int a, int b, int p)
{
LL res = 1 % p;
while (b)
{
if (b & 1) res = res * a % p;
a = (LL)a * a % p;
b >>= 1;
}
return res;
}
2.快速幂求逆元
#include <iostream>
using namespace std;
typedef long long LL;
LL qmi(int a, int b, int p)
{
LL res = 1 % p;
while (b)
{
if (b & 1) res = res * a % p;
a = (LL)a * a % p;
b >>= 1;
}
return res;
}
int main()
{
int n;
cin>>n;
while (n -- )
{
int a, p;
cin>>a>>p;
/*
a 和 p 互质并且 p 是质数时 a 的逆元为 a^p-2 mod p
因 p 为质数,因子只有 1 和 p,所以 a 和 p 存在除 1 以外的公因数即不互质的话,那个公因数只能是 p
所以只需要判断 p 是否为 a 的因子就可以确定 a 和 p 是否互质
*/
if (a % p == 0) puts("impossible");
else cout<<qmi(a, p - 2, p)<<endl;
}
return 0;
}
扩展欧几里得算法
1.扩展欧几里得算法
#include <iostream>
#include <algorithm>
using namespace std;
int exgcd(int a, int b, int &x, int &y)//必须传引用,不能用全局变量
{
//b 为 0 时,最大公约数为 a ,所以 a x 1 + 0 x 0 = a
if (!b)
{
x = 1, y = 0;
return a;
}
//b 不为 0 时,详细过程见手动模拟
int d = exgcd(b, a % b, y, x);
y -= a / b * x;//调用函数求得最大公约数之后发现 y 要减 a / b * x 才是 b 的真正的系数
return d;
}
int main()
{
int n;
scanf("%d", &n);//数据范围比较大时用scanf会快很多
while (n -- )
{
int a, b;
scanf("%d%d", &a, &b);
int x, y;
exgcd(a, b, x, y);
printf("%d %d\n", x, y);
}
return 0;
}
// 求x, y,使得ax + by = gcd(a, b)
int exgcd(int a, int b, int &x, int &y)
{
if (!b)
{
x = 1; y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);
y -= (a/b) * x;
return d;
}
2.线性同余方程
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
int exgcd(int a, int b, int &x, int &y)
{
if (!b)
{
x = 1, y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
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);
// b 不是 a 和 m 的最大公约数的倍数 ,无解
if (b % d) puts("impossible");
//ax + by = gcd ,ax0 + by0 = b 解得 x0 = x * b / gcd , y0 = y * b / gcd(扩大 b / gcd 倍)
/*
由题可知 b 的范围几乎就是 int 的范围,所以要转成 LL ,防止x * b结果溢出,但是题中所说输出答案必须在 int 范围之内,所以用 %d 保证输出答案为 int(%lld 为 long long)
*/
else printf("%d\n", (LL)x * b / d % m);
}
return 0;
}
中国剩余定理(*)
表达整数的奇怪方式
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL exgcd(LL a, LL b, LL &x, LL &y)
{
if (!b)
{
x = 1, y = 0;
return a;
}
LL d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
int main()
{
int n;
cin >> n;
LL x = 0, m1, a1;
cin >> m1 >> a1;
for (int i = 0; i < n - 1; i ++ )
{
LL m2, a2;
cin >> m2 >> a2;
LL k1, k2;
LL d = exgcd(m1, m2, k1, k2);
if ((a2 - a1) % d)
{
x = -1;
break;
}
k1 *= (a2 - a1) / d;
k1 = (k1 % (m2/d) + m2/d) % (m2/d);
x = k1 * m1 + a1;
LL m = abs(m1 / d * m2);
a1 = k1 * m1 + a1;
m1 = m;
}
if (x != -1) x = (a1 % m1 + m1) % m1;
cout << x << endl;
return 0;
}
高斯消元(*)
1.高斯消元解线性方程组
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 110;
const double eps = 1e-8;
int n;
double a[N][N];
int gauss() // 高斯消元,答案存于a[i][n]中,0 <= i < n
{
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;
for (int i = c; i <= n; 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[i][j] * a[j][n];
return 0; // 有唯一解
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n + 1; j ++ )
scanf("%lf", &a[i][j]);
int t = gauss();
if (t == 2) puts("No solution");
else if (t == 1) puts("Infinite group solutions");
else
{
for (int i = 0; i < n; i ++ )
printf("%.2lf\n", a[i][n]);
}
return 0;
}
2.高斯消元解异或线性方程组
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n;
int 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 (a[i][c])
t = i;
if (!a[t][c]) continue;
for (int i = c; i <= n; i ++ ) swap(a[r][i], a[t][i]);
for (int i = r + 1; i < n; i ++ )
if (a[i][c])
for (int j = n; j >= c; j -- )
a[i][j] ^= a[r][j];
r ++ ;
}
if (r < n)
{
for (int i = r; i < n; i ++ )
if (a[i][n])
return 2;
return 1;
}
for (int i = n - 1; i >= 0; i -- )
for (int j = i + 1; j < n; j ++ )
a[i][n] ^= a[i][j] * a[j][n];
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 ++ ) cout << a[i][n] << endl;
}
else if (t == 1) puts("Multiple sets of solutions");
else puts("No solution");
return 0;
}
求组合数
1.求组合数 I
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2010, mod = 1e9 + 7;
int c[N][N];
void init()
{
c[0][0] = 1;
for (int i = 1; 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;
}
int main()
{
int n;
init();
scanf("%d", &n);
while (n -- )
{
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", c[a][b]);
}
return 0;
}
2.求组合数 II
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, mod = 1e9 + 7;
//fact 存的是元素的阶乘,infact 存的是元素的阶乘的逆元
int fact[N], infact[N];
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int main()
{
//先初始化 0 的阶乘和阶乘逆元
fact[0] = infact[0] = 1;
//初始化阶乘数组和阶乘逆元数组
for (int i = 1; i < N; i ++ )
{
//fact[i] = fact[i - 1] * i
fact[i] = (LL)fact[i - 1] * i % mod;
//infact[i] = infact[i - 1] * i 模 mod 的逆元
infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
int n;
scanf("%d", &n);
while (n -- )
{
int a, b;
scanf("%d%d", &a, &b);
//fact[a] * infact[b] 会溢出 1e9 了 ,所以要转 LL 并且先模 mod 一下 ,公式见上图
printf("%d\n", (LL)fact[a] * infact[b] % mod * infact[a - b] % mod);
}
return 0;
}
int qmi(int a, int k, int p) // 快速幂模板
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
// 预处理阶乘的余数和阶乘逆元的余数
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ )
{
fact[i] = (LL)fact[i - 1] * i % mod;
infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
3.求组合数 III
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int C(int a, int b, int p)
{
if (b > a) return 0;//注意 b > a 的情况
int res = 1;
for (int i = 1, j = a; i <= b; i ++, j -- )
{
// a 到 a - b + 1 总共 a - b 个数
res = (LL)res * j % p;
//再乘 1 到 b 的逆元也就是 b 的阶乘的逆元
res = (LL)res * qmi(i, p - 2, p) % p;
}
return res;
}
int lucas(LL a, LL b, int p)
{
//递归终点 a < p && b < p
if (a < p && b < p) return C(a, b, p);
return (LL)lucas(a / p, b / p, p) * C(a % p, b % p, p) % p;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
LL a, b;//注意 a 和 b 的类型
int p;
cin >> a >> b >> p;
cout << lucas(a, b, p) << endl;
}
return 0;
}
//若 p 是质数,C(n, m) = C(n / p, m / p) * C(n % p, m % p) (mod p)
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int C(int a, int b, int p)
{
if (b > a) return 0;
int res = 1;
for (int i = 1, j = a; i <= b; i ++, j -- )
{
res = (LL)res * j % p;
res = (LL)res * qmi(i, p - 2, p) % p;
}
return res;
}
int lucas(LL a, LL b, int p)
{
if (a < p && b < p) return C(a, b, p);
return (LL)lucas(a / p, b / p, p) * C(a % p, b % p, p) % p;
}
4.求组合数 Ⅳ
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 5010;
int primes[N], cnt;
int sum[N];
bool st[N];
//线性筛法筛质数
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
//返回 n 的阶乘中质因子 p 的次数
int get(int n, int p)
{
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)
{
c.push_back(t % 10);
t /= 10;
}
return c;
}
int main()
{
int a, b;
cin >> a >> b;
//筛出 1 到 a 的质数,因为 b 和 a - b 都比 a 小,所以 1 到 b 和 a - b的质数包含在 1 到 a 的质数中
get_primes(a);
//求出1 到 a ,1 到 b 和 a - b每个质数的总次数
for (int i = 0; i < cnt; i ++ )
{
int p = primes[i];
//C(a,b) 中 p 的总次数等于 a! 中 p 的次数减去 a - b! 中 p 的次数减去b! 中 p 的次数
sum[i] = get(a, p) - get(a - b, p) - get(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]);
//输出最终结果
for (int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);
puts("");
return 0;
}
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
1. 筛法求出范围内的所有质数
2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + ...
3. 用高精度乘法将所有质因子相乘
int primes[N], cnt; // 存储所有质数
int sum[N]; // 存储每个质数的次数
bool st[N]; // 存储每个数是否已被筛掉
void get_primes(int n) // 线性筛法求素数
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int get(int n, int p) // 求n!中 p 的次数
{
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)
{
c.push_back(t % 10);
t /= 10;
}
return c;
}
get_primes(a); // 预处理范围内的所有质数
for (int i = 0; i < cnt; i ++ ) // 求每个质因数的次数
{
int p = primes[i];
sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}
vector<int> res;
res.push_back(1);
for (int i = 0; i < cnt; i ++ ) // 用高精度乘法将所有质因子相乘
for (int j = 0; j < sum[i]; j ++ )
res = mul(res, primes[i]);
5.满足条件的01序列
/*给定n个0和n个1,它们按照某种顺序排成长度为2n的序列
满足任意前缀中0的个数都不少于1的个数的序列的数量为: Cat(n) = C(2n, n) / (n + 1)*/
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, mod = 1e9 + 7;
int qmi(int a, int b, int p)
{
int res = 1;
while (b)
{
if (b & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
b >>= 1;
}
return res;
}
int C(int a,int b, int p){
if(b > a) return 0;
int res = 1;
for(int i = 1, j = a; i <= b; i++,j--){
res = (LL)res * j % p;
res = (LL)res * qmi(i, p - 2, p) % p;
}
return res;
}
int main()
{
int n;
cin >> n;
cout<<(LL)C(2 * n, n, mod) * qmi(n + 1, mod - 2, mod) % mod<<endl;
return 0;
}
容斥原理
能被整除的数
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 20;
int p[N];
int main()
{
int n, m;
cin >> n >> m;
//用 p 数组存储 m 个质数
for (int i = 0; i < m; i ++ ) cin >> p[i];
int res = 0;
//外层循环遍历从1 到 1111...(m个1,2^m - 1)的每一个数字,每个数字代表上图中第二幅图等式右边的一个加项
for (int i = 1; i < 1 << m; i ++ )
{
int t = 1, s = 0;//t 为选中集合对应质数的乘积 ,s 为选中集合的数量
//内层循环,确定当前加项的具体形式,从最低位开始遍历当前数字的每一位,每个数字都当成有 m 位,看当前数字有多少个 1,确定当前项是哪个集合或哪几个集合的交集
for (int j = 0; j < m; j ++ )
if (i >> j & 1)
{
//若 t * p[j] > n ,则 n / t = 0,无意义
if ((LL)t * p[j] > n)
{
t = -1;
break;
}
t *= p[j];//乘上集合对应的质数
s ++ ;//有一个 1 ,集合数量加一
}
if (t != -1)
{
//除对应集合的质数的乘积就是集合的数量
if (s % 2) res += n / t;//奇数项为正
else res -= n / t;//偶数项为负
}
}
cout << res << endl;
return 0;
}
for (int i = 0; i < m; i ++ ) cin >> p[i];
int res = 0;
for (int i = 1; i < 1 << m; i ++ )
{
int t = 1, s = 0;
for (int j = 0; j < m; j ++ )
if (i >> j & 1)
{
if ((LL)t * p[j] > n)
{
t = -1;
break;
}
t *= p[j];
s ++ ;
}
if (t != -1)
{
if (s % 2) res += n / t;
else res -= n / t;
}
}
博弈论
1.Nim游戏
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
int res = 0;
while (n -- )
{
int x;
scanf("%d", &x);
res ^= x;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
2.台阶-Nim游戏
//奇数台阶上的值的异或值为非0,则先手必胜,反之必败!
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int main()
{
int n;
scanf("%d", &n);
int res = 0;
for (int i = 1; i <= n; i ++ )
{
int x;
scanf("%d", &x);
//奇数二进制形式最低位为 1 ,&1 后为 1,偶数为 0
if (i & 1) res ^= x;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
3.集合-Nim游戏
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110, M = 10010;
int n, k;
int s[N], f[M];//s 存储的是可拿取的石子数量,f 存储的是存储已经计算过的状态(石子数量)的 Nim 值,避免重复计算
int sg(int x)
{
if (f[x] != -1) return f[x];
// S 要在函数内部定义 ,因为每次递归 S 都是针对当前状态的,S用于当前状态之后的状态的sg值
unordered_set<int> S;
//递归计算当前状态 x 可以到达的之后的所有状态的值插入 S(x 能否减可拿取石子数量集合 S 中的元素)
for (int i = 0; i < k; i ++ )
{
int sum = s[i];
if (x >= sum) S.insert(sg(x - sum));
}
//递归完之后就剩当前状态 x 没有值(不在 S 集合中)了,遍历自然数,如果有不在 S 中的自然数便是当前状态 x 的 sg 值
for (int i = 0; ; i ++ )
if (!S.count(i))
return f[x] = i;
}
int main()
{
cin >> k;
for (int i = 0; i < k; i ++ ) cin >> s[i];
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
for (int i = 0; i < n; i ++ )
{
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
4.拆分-Nim游戏
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
int f[N];
int sg(int x)
{
if (f[x] != -1) return f[x];
unordered_set<int> S;
//x > i >= j
for (int i = 0; i < x; i ++ )
for (int j = 0; j <= i; j ++ )
S.insert(sg(i) ^ sg(j));
for (int i = 0;; i ++ )
if (!S.count(i))
return f[x] = i;
}
int main()
{
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
while (n -- )
{
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
NIM游戏 —— 模板题 AcWing 891. Nim游戏
给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。
我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。
定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0
公平组合游戏ICG
若一个游戏满足:
- 由两名玩家交替行动;
- 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
- 不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
有向图游戏
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。
Mex运算
设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S
SG函数
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:
SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。
有向图游戏的和 —— 模板题 AcWing 893. 集合-Nim游戏
设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)
定理
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
第五讲 动态规划
背包问题
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int f[N];//f[j] 表示背包容量为 j 时能获得的最大价值。
int main()
{
cin >> n >> m;
while(n--){
int v, w;
cin >> v >> w;
//逆序遍历(确保在更新 f[j] 的时候,使用的是上一轮迭代中的值 f[j - v[i]])
for (int j = m; j >= v; j -- ){
//选择不放入当前物品和放入当前物品中的最大价值。
f[j] = max(f[j], f[j - v] + w);
}
}
cout << f[m] << endl;
return 0;
}
1, s = 0;//t 为选中集合对应质数的乘积 ,s 为选中集合的数量
//内层循环,确定当前加项的具体形式,从最低位开始遍历当前数字的每一位,每个数字都当成有 m 位,看当前数字有多少个 1,确定当前项是哪个集合或哪几个集合的交集
for (int j = 0; j < m; j ++ )
if (i >> j & 1)
{
//若 t * p[j] > n ,则 n / t = 0,无意义
if ((LL)t * p[j] > n)
{
t = -1;
break;
}
t *= p[j];//乘上集合对应的质数
s ++ ;//有一个 1 ,集合数量加一
}
if (t != -1)
{
//除对应集合的质数的乘积就是集合的数量
if (s % 2) res += n / t;//奇数项为正
else res -= n / t;//偶数项为负
}
}
cout << res << endl;
return 0;
}
```c++
for (int i = 0; i < m; i ++ ) cin >> p[i];
int res = 0;
for (int i = 1; i < 1 << m; i ++ )
{
int t = 1, s = 0;
for (int j = 0; j < m; j ++ )
if (i >> j & 1)
{
if ((LL)t * p[j] > n)
{
t = -1;
break;
}
t *= p[j];
s ++ ;
}
if (t != -1)
{
if (s % 2) res += n / t;
else res -= n / t;
}
}
博弈论
1.Nim游戏
[外链图片转存中…(img-zuhhwJUx-1739004830487)]
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
int res = 0;
while (n -- )
{
int x;
scanf("%d", &x);
res ^= x;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
2.台阶-Nim游戏
[外链图片转存中…(img-2CUrw937-1739004830487)]
//奇数台阶上的值的异或值为非0,则先手必胜,反之必败!
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int main()
{
int n;
scanf("%d", &n);
int res = 0;
for (int i = 1; i <= n; i ++ )
{
int x;
scanf("%d", &x);
//奇数二进制形式最低位为 1 ,&1 后为 1,偶数为 0
if (i & 1) res ^= x;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
3.集合-Nim游戏
[外链图片转存中…(img-plN5tdjK-1739004830487)]
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110, M = 10010;
int n, k;
int s[N], f[M];//s 存储的是可拿取的石子数量,f 存储的是存储已经计算过的状态(石子数量)的 Nim 值,避免重复计算
int sg(int x)
{
if (f[x] != -1) return f[x];
// S 要在函数内部定义 ,因为每次递归 S 都是针对当前状态的,S用于当前状态之后的状态的sg值
unordered_set<int> S;
//递归计算当前状态 x 可以到达的之后的所有状态的值插入 S(x 能否减可拿取石子数量集合 S 中的元素)
for (int i = 0; i < k; i ++ )
{
int sum = s[i];
if (x >= sum) S.insert(sg(x - sum));
}
//递归完之后就剩当前状态 x 没有值(不在 S 集合中)了,遍历自然数,如果有不在 S 中的自然数便是当前状态 x 的 sg 值
for (int i = 0; ; i ++ )
if (!S.count(i))
return f[x] = i;
}
int main()
{
cin >> k;
for (int i = 0; i < k; i ++ ) cin >> s[i];
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
for (int i = 0; i < n; i ++ )
{
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
4.拆分-Nim游戏
[外链图片转存中…(img-648C9WXa-1739004830487)]
[外链图片转存中…(img-rblr5h4h-1739004830487)]
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
int f[N];
int sg(int x)
{
if (f[x] != -1) return f[x];
unordered_set<int> S;
//x > i >= j
for (int i = 0; i < x; i ++ )
for (int j = 0; j <= i; j ++ )
S.insert(sg(i) ^ sg(j));
for (int i = 0;; i ++ )
if (!S.count(i))
return f[x] = i;
}
int main()
{
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
while (n -- )
{
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
NIM游戏 —— 模板题 AcWing 891. Nim游戏
给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。
我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。
定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0
公平组合游戏ICG
若一个游戏满足:
- 由两名玩家交替行动;
- 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
- 不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
有向图游戏
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。
Mex运算
设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S
SG函数
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:
SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。
有向图游戏的和 —— 模板题 AcWing 893. 集合-Nim游戏
设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)
定理
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
第五讲 动态规划
背包问题
[外链图片转存中…(img-VoRbyGZW-1739004830487)]
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int f[N];//f[j] 表示背包容量为 j 时能获得的最大价值。
int main()
{
cin >> n >> m;
while(n--){
int v, w;
cin >> v >> w;
//逆序遍历(确保在更新 f[j] 的时候,使用的是上一轮迭代中的值 f[j - v[i]])
for (int j = m; j >= v; j -- ){
//选择不放入当前物品和放入当前物品中的最大价值。
f[j] = max(f[j], f[j - v] + w);
}
}
cout << f[m] << endl;
return 0;
}