-3.由数据范围反推算法复杂度及算法内容
-2.输入加速(cin,cout)
ios::sync_with_stdio(false);//加速几百毫秒 cin.tie(0); // 接近scanf cout.tie(0);
-1.万能递推公式
代码
只需要写出前十个递推结果就行
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <cassert>
#include<iostream>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)
#define pb push_back
#define mp make_pair
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef vector<int> VI;
typedef long long ll;
typedef pair<int,int> PII;
const ll mod=1000000007;
ll powmod(ll a,ll b) {ll res=1;a%=mod; assert(b>=0); for(;b;b>>=1){if(b&1)res=res*a%mod;a=a*a%mod;}return res;}
// head
int _;
ll n;
namespace linear_seq {
const int N=10010;
ll res[N],base[N],_c[N],_md[N];
vector<int> Md;
void mul(ll *a,ll *b,int k) {
rep(i,0,k+k) _c[i]=0;
rep(i,0,k) if (a[i]) rep(j,0,k) _c[i+j]=(_c[i+j]+a[i]*b[j])%mod;
for (int i=k+k-1;i>=k;i--) if (_c[i])
rep(j,0,SZ(Md)) _c[i-k+Md[j]]=(_c[i-k+Md[j]]-_c[i]*_md[Md[j]])%mod;
rep(i,0,k) a[i]=_c[i];
}
int solve(ll n,VI a,VI b) { // a 系数 b 初值 b[n+1]=a[0]*b[n]+...
// printf("%d\n",SZ(b));
ll ans=0,pnt=0;
int k=SZ(a);
assert(SZ(a)==SZ(b));
rep(i,0,k) _md[k-1-i]=-a[i];_md[k]=1;
Md.clear();
rep(i,0,k) if (_md[i]!=0) Md.push_back(i);
rep(i,0,k) res[i]=base[i]=0;
res[0]=1;
while ((1ll<<pnt)<=n) pnt++;
for (int p=pnt;p>=0;p--) {
mul(res,res,k);
if ((n>>p)&1) {
for (int i=k-1;i>=0;i--) res[i+1]=res[i];res[0]=0;
rep(j,0,SZ(Md)) res[Md[j]]=(res[Md[j]]-res[k]*_md[Md[j]])%mod;
}
}
rep(i,0,k) ans=(ans+res[i]*b[i])%mod;
if (ans<0) ans+=mod;
return ans;
}
VI BM(VI s) {
VI C(1,1),B(1,1);
int L=0,m=1,b=1;
rep(n,0,SZ(s)) {
ll d=0;
rep(i,0,L+1) d=(d+(ll)C[i]*s[n-i])%mod;
if (d==0) ++m;
else if (2*L<=n) {
VI T=C;
ll c=mod-d*powmod(b,mod-2)%mod;
while (SZ(C)<SZ(B)+m) C.pb(0);
rep(i,0,SZ(B)) C[i+m]=(C[i+m]+c*B[i])%mod;
L=n+1-L; B=T; b=d; m=1;
} else {
ll c=mod-d*powmod(b,mod-2)%mod;
while (SZ(C)<SZ(B)+m) C.pb(0);
rep(i,0,SZ(B)) C[i+m]=(C[i+m]+c*B[i])%mod;
++m;
}
}
return C;
}
int gao(VI a,ll n) {
VI c=BM(a);
c.erase(c.begin());
rep(i,0,SZ(c)) c[i]=(mod-c[i])%mod;
return solve(n,c,VI(a.begin(),a.begin()+SZ(c)));
}
};
int main() {
int n;
// int t;
// cin>>t;
// while (t--) {
scanf("%lld",&n);
printf("%d\n",linear_seq::gao(VI{1,5,13,25,41,61,85,113,145,181},n-1));
//}
}
0.递归
思想
递归就是有去(递去)有回(归来),如下图所示。“有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决,就像上面例子中的钥匙可以打开后面所有门上的锁一样;“有回”是指 : 这些问题的演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。
递归的三要素:
1、明确递归终止条件;
2、给出递归终止时的处理办法;
3、提取重复的逻辑,缩小问题规模。
1). 明确递归终止条件
我们知道,递归就是有去有回,既然这样,那么必然应该有一个明确的临界点,程序一旦到达了这个临界点,就不用继续往下递去而是开始实实在在的归来。换句话说,该临界点就是一种简单情境,可以防止无限递归。
2). 给出递归终止时的处理办法
我们刚刚说到,在递归的临界点存在一种简单情境,在这种简单情境下,我们应该直接给出问题的解决方案。一般地,在这种情境下,问题的解决方案是直观的、容易的。
3). 提取重复的逻辑,缩小问题规模*
我们在阐述递归思想内涵时谈到,递归问题必须可以分解为若干个规模较小、与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。从程序实现的角度而言,我们需要抽象出一个干净利落的重复的逻辑,以便使用相同的方式解决子问题
模型一: 在递去的过程中解决问题
function recursion(大规模){
if (end_condition){ // 明确的递归终止条件
end; // 简单情景
}else{ // 在将问题转换为子问题的每一步,解决该步中剩余部分的问题
solve; // 递去
recursion(小规模); // 递到最深处后,不断地归来
}
}
模型二: 在归来的过程中解决问题
function recursion(大规模){
if (end_condition){ // 明确的递归终止条件
end; // 简单情景
}else{ // 先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题
recursion(小规模); // 递去
solve; // 归来
}
}
1.快排
方法
1.确定分界点(可以是p[0],p[n],p[n/2],随机)
2.(重点)调整区间(将<=n的放到左侧,把>=n的放到右侧)分成两部分
用两个指针分别指两端,先后向中间靠拢
3.递归处理左右两边。
(不稳定)
可以想办法让每个数不一样:比如把里面的元素变成二元组a=><a,i>。
代码
1.暴力
//用空间换时间,多开两个数组
int a[n];
int b[n];
int p[2*n];//排p序
int n1=0,n2=0;
for (int i=0;i<=2*n;i++)
{
if(p[i]>=p[0])
{
a[n1++]=p[i];
}else if(p[i]<p[0])
{
b[n2++]=p[i];
}
}
2.优美而简洁 q[]
#include<iostream>
using namespace std;
const int N =1e6+10;
int n;
int q[N];
void quick_sort(int q[],int l,int r)
{
if(l>=r) return ;//如果数组里只有1个或没有数,直接结束
//1.int q[r] //2.int q[(l+r+1)/2] (有时候用两边会超时)
int x=q[l],i=l-1,j=r+1;//因为是先同时向中间靠拢,先把两个指针放在数组外面
//
while(i<j)//i不能大于j否则结束
{
do i++;while(q[i]<x);//向中间走直到遇到大于x的
do j--;while(q[j]>x);//向中间走直到遇到小于x的
if(i<j) swap(q[i],q[j]);//将两个数交换位置,再继续走
}
//quick_sort(q,l,i-1)
//quick_sort(q,i,r)
quick_sort(q,l,j);//递归
quick_sort(q,j+1,r);
/*eg:
2
1 2
用i则不能取到左边界,把x取值改成向上取整
用j则不能取到右边界,把x取值改成向下取整
取到边界会导致递归死循环
***************************
以j为划分时,x不能选q[r] (若以i为划分,则x不能选q[l])
假设 x = q[r]
关键句子quick_sort(q, l, j), quick_sort(q, j + 1, r);
由于j的最小值是l,所以q[j+1..r]不会造成无限划分
但q[l..j](即quick_sort(q, l, j))却可能造成无限划分,因为j可能为r
举例来说,若x选为q[r],数组中q[l..r-1] < x,
那么这一轮循环结束时i = r, j = r,显然会造成无限划分
*/
}
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;
}
注意:由于使用do-while循环,所以i和j一定会!!!自增!!!,使得循环会继续下去,但是如果采用while循环(i和j的初始化做出对应的变更),i和j在特殊情况下不自增的话,循环就会卡死
例如:
while(q[i] < x) i++;
while(q[j] > x) j--;
当q[i]和q[j]都为 x 时, i 和 j 都不会更新,导致 while 陷入死循环
2.分并排序
方法
1.确定分界点:mid (0+n)/2
2.递归排序left,right(分成两个数组)
3.(重点)归并---合二为一
用两个指针从开头比较
(稳定)
遇到相同的先把第一个输出,保证数组里的相同数的位置是不变的
先分成好几段
再和并
代码
优雅的分并
#include<iostream>
using namespace std;
const int N=1e6+10;
int n;
int q[N],tmp[N];
void merge_sort(int q[],int l,int r)
{
if(l>=r) return;//如果数组小于等于一个,直接出去
int mid=(l+r)>>1;//相当于除于二(把l+r看作二进制向右退一位,向下取整)(l+r<<1,相当于乘以2)
merge_sort(q,l,mid),merge_sort(q,mid+1,r);//递归分成好多段
/*为什么不用 mid - 1 作为分隔线呢
即 merge_sort(q, l, mid - 1 ), merge_sort(q, mid, r)
因为 mid = l + r >> 1 是向下取整,mid 有可能取到 l (数组只有两个数时),造成无限划分
解决办法: mid 向上取整就可以了, 即 mid = l + r + 1 >> 1
*/
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++];//如果有一半有剩余的全给tmp数组
while(j<=r) tmp[k++]=q[j++];
for(i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];//把tmp再还给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;
}
3.二分
方法
如果数组满足某种性质,一半满足另一半不满足,二分可以找到这个性质的边界
整数二分代码
//区间[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是否满足性质(mid)>=x
else l=mid+1;
}
return l;//左边界
}//假如是找x的边界,q[mid]>=x;,后面检查如果q[l]!=x,则没找到
//区间[l,r]被分成[l,mid-1]和[mid,r]时使用
int bsearch_2(int l,int r)
{
m
while(l<r)
{
int mid=l+r+1>>1;
if(check(mid)) l=mid;//(mid)<=x;
else r=mid-1;
}
}
//加入l=r-1,则mid=l,如果是ture,则死循环
//根据这两个可以找到一个性质的左右边界
实数二分代码
int bsearch(double l,double r)
{
//后者循环100次for(int i=0;i<100;i++)
while(r-l>=1e-6)//差值很小的时候,可以近似认为找到了
{
double mid=(l+r)/2;
if(check(mid)) r=mid;//例如找平方根if(mid*mid>=x)
else l=mid;
}
}
4.高精度
方法
1.A+B(大)
2.A-B
3.A*a 位数len(A)<=1e6,len(a)<=9 一般
4.A/a
5.A*B(略)
6.A/B(略)
一 : 存大数字
用数组,倒序存入个位放开头(进行加时,进位方便)
二:依次经行运算
1.加法
#include<iostream>
#include<vector>
using namespace std;
// C=A+B
vector<int> add(vector<int> &A,vector<int> &B)//&是引用,不加&会把数组copy一遍,加上&就会快好多
{
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;
vector<int> A,B;
cin>>a>>b;//a="123456"
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');
auto C=add(A,B);//C++11 auto可以在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
return 0;
}
2.减法
#include<iostream>
#include<vector>
using namespace std;
// C=A-B
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;
}
vector<int> sub(vector<int> &A,vector<int> &B)//&是引用,不加&会把数组copy一遍,加上&就会快好多
{
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;
vector<int> A,B;
cin>>a>>b;//a="123456"
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');
auto C=sub(A,B);//C++11 auto可以在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型
if(cmp(A,B))
{
auto C=sub(A,B);
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
}
else
{
auto C=sub(B,A);
printf("-");
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
}
return 0;
}
3.乘法
#include<iostream>
#include<vector>
using namespace std;
// C=A*B
vector<int> mul(vector<int> &A,int b)//&是引用,不加&会把数组copy一遍,加上&就会快好多
{
vector<int> C;
int t=0;
for(int i=0;i<A.size()||t;i++)
{
t+=A[i]*b;
C.push_back(t%10);
t/=10;
}
//如果for里面||t,则这个就不用了if(t) C.push_back(t);//多的
while(C.size()>1&&C.back()==0) C.pop_back();//前导0;
return C;
}
int main()
{
string a;
vector<int> A;
int b;
cin>>a>>b;//a="123456"
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');//A=[6,5,4,3,2,1]
auto C=mul(A,b);//C++11 auto可以在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
return 0;
}
4.除法
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// C=A+B
vector<int> div(vector<int> &A,int b,int &t)//&是引用,不加&会把数组copy一遍,加上&就会快好多.....&t是引用主函数的t,相当于一个变量两个名字,改变引用名字的值,引用变量的值也会相应改变,比指针简便
{
vector<int> C;
t=0;
for(int i=A.size();i>=0;i--)
{
t=t*10+A[i];
C.push_back(t/b);
t%=b;
}
reverse(C.begin(),C.end());//reverse 可以反转字符串,字符数组,整型数组
//如果for里面||t,则这个就不用了if(t) C.push_back(t);//多的
while(C.size()>1&&C.back()==0) C.pop_back();//前导0;
return C;
}
int main()
{
string a;
vector<int> A;
int b;
cin>>a>>b;//a="123456"
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');//A=[6,5,4,3,2,1]
int t;
auto C=div(A,b,t);//C++11 auto可以在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
cout<<endl<<t;
return 0;
}
5.前缀和
方法
一维:
a[]数组最好从1开始
s[i]=a[i]+a[i-1]+a[i-2]....+a[2]+a[1];
s[0]=0;
s[i]=s[i-1]+a[i];
要求 [1,10] 则 sum=s[10]-s[0];
二维:
前缀和数组:i,j点的上面加上左边减去相交的,最后加上a[i] [j]
代码
//一维
iOS::sync_with_stdio(false);//取消同步,提高cin读取速度,但是不能使用scanf
//数据大于一百万,建议用scanf
for (int i=1;i<=n;i++)
{
cin>>a[i];
s[i]=s[i-1]+a[i];
}
//求一段的和
sum[1,10]=s[10]-s[0];
//二维前缀和 注:二维数组开太大会报错,内存=n*n*sizeof(int)/1024
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
cin>>a[i][j];
s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];
}
//求小矩形的和
sum[x1,y1;x2,y2]=s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]
//都是x1,y1减一;
6.差分
方法
一维
前缀和的逆运算
构造一个数组b[i]=a[i]-a[i-1];
对b数组累加得原数组
求一段数组全部加一个数
[l,r]+c: b[l]+c,b[r+1]-c; (0(1))
二维
构造差分数组 b[i] [j]=a[i] [j]-a[i-1] [j]-a[i] [j-1]+a[i-1] [j-1];
求一个小矩形加上一个数[x1,y1;x2,y2]+c:
b[x1] [y1]+c,b[x2+1] [y1]-c,b[x1] [y2+1]-c,b[x2+1] [y2+1]+c;
代码
//一维
#include<iostream>
using namespace std;
const int N=1e6;
int a[N],b[N];
int n,m;
void insert(int l,int r,int c) // 核心公式
{
b[l]+=c;
b[r+1]-=c;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
insert(i,i,a[i]);//直接构造差分数组
}
while(m--)
{
int l,r,c;
scanf("%d %d %d",&l,&r,&c);
insert(l,r,c);
}
for(int i=1;i<=n;i++)
{
b[i]+=b[i-1];//复原
printf("%d ",b[i]);
}
return 0;
}
//二维
#include<iostream>
using namespace std;
const int N=1010;
int a[N][N];
int b[N][N];
int n,m,q;
void insert(int x1,int y1,int x2,int y2,int c) //核心公式
{
b[x1][y1]+=c;
b[x2+1][y1]-=c;
b[x1][y2+1]-=c;
b[x2+1][y2+1]+=c;
}
int main()
{
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]);
insert(i,j,i,j,a[i][j]);//构造差分数组
}
}
while(q--)
{
int x1,y1,x2,y2,c;
scanf("%d %d %d %d %d",&x1,&y1,&x2,&y2,&c);
insert(x1,y1,x2,y2,c);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];
printf("%d ",b[i][j]);
}
printf("\n");
}
}
7.双指针
方法
先用暴力方法,看看i和j有没有什么单调关系(向一个方向),来套模板
暴力用两个for循环是o(n^2),用双指针可以优化到o(n)
代码
for(int i=0,j=0;i<n;i++) //n次
{
while(j<=i&&check(i,j)) j++; // 最多n次 一共最多是o(n+n)
.....
}
//i在最前面走,符合条件,j也向前走,最多走o(2n)步
//例最长不重复子序列
#include<iostream>
using namespace std;
int n;
const int N=1e6;
int a[N];
int s[N];
int main()
{
scanf("%d",&n);
for (int i = 0 ; i<n;i++) scanf("%d",&a[i]);
int res=0;
for(int i=0,j=0;i<n;i++)
{
s[a[i]]++; //判重,map也可以;
while(s[a[i]]>1)//不用写j<=i,如果条件不符合,那么j,i之间一定没有重复元素;
{
s[a[j]]--;//如果重复了,j向前移动,并且删除之前的标记,直到最后重复的数字,重新开始判断;
j++;
}
res=max(res,i-j+1);//取最大值
}
printf("%d\n",res);
return 0;
}
8.位运算
方法
主要问题:
n的二进制表示中第k位是几?
15=(1111)
1.把第k位移到最后一位
2.看个位是几(x&1)
总:n>>k&1;
返回最后一位1
(可以统计1的个数)
lowbit:
返回x的最后一位1是多少
x=1010000 lowbit(x)=10000;
x&-x
-x是补码 是取反加一(~x+1)
例:
x=1010.....1000000;
~x=0101....0111111;
-x=0101....1000000;
//二进制中1的个数
#include<iostream>
using namespace std;
int lowbit(int x)
{
return x&-x;
}
int main()
{
int n;
scanf("%d",&n);
while(n--)
{
int x;
cin>>x;
int res=0;
while(x) x-=lowbit(x),res++;//每次减去最后一位1
cout<<res<<" ";
}
return 0;
}
//m的n次方
/*例如 n = 13,则 n 的二进制表示为 1101, 那么 m 的 13 次方可以拆解为:
m^1101 = m^0001 * m^0100 * m^1000。
我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解:
*/
int pow(int mint n){
int sum = 1;
int tmp = m;
while(n != 0){
if(n & 1 == 1){
sum *= tmp;
}
tmp *= tmp;
n = n >> 1;
}
return sum;
}
9.离散化(整数保序)
方法
例:值域比较大(10^9)(做下标),个数比较小(10^5)
把他们映射到从0开始的连续的自然数(10^5)
a[] : 1 3 100 2000 100000000
0 1 2 3 4
1.可能有重复的 (去重)
vector <int> alls;//储存所以待离散化的值
sort(alls.begin(),alls.end());//将所以值排序
alls.erase(unique(alls.begin(),alls.end()),alls.end());//去掉重复元素
//erase是删除,,,unique是去重把重复的扔后面返回最后一个下标,,,总::把重复扔后面然后删除
2.如何算出a[i]离散化后的值 (二分找下标)
//二分求出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 3,不加1是从0开始
}
代码()
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int,int> PII;
const int N=300010;
int n,m;
int a[N],s[N];
vector<int> alls;
vector<PII> add,query;
int find(int 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开始
}
int main()
{
cin>> n>> m;
for(int i=0;i<n;i++)
{
int x,c;
cin>>x>>c;
add.push_back({x,c});//用的是pair所以用大括号括起来
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;
}
//预处理前缀和
for(int i=1;i<=alls.size();i++) s[i]+=s[i-1]+a[i];
//处理询问
for(auto item : query )
{
int l = find(item.first),r=find(item.second);
cout<<s[r]-s[l-1]<<endl;
}
return 0;
}
10.区间合并
方法
把有交集的或者端点重合的合并
代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int,int> PII;
vector<PII> segs;
const int N=1e6+10;
int n;
void merge(vector <PII> &segs)
{
vector <PII> res;
sort(segs.begin(),segs.end());//对于pair类型,先排第一个数
int st=-2e9,ed=-2e9;
for(auto seg : segs)//相当于双指针
{
if(ed<seg.first)//第一次直接更新区间
{
if(st!=-2e9) res.push_back({st,ed});
st=seg.first,ed=seg.second;
}
else ed =max(ed,seg.second);
}
if(st!=-2e9) res.push_back({st,ed});//收尾存入最后一个,或者如果只有一个,就一个了
segs=res;
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
int l,r;
cin>>l>>r;
segs.push_back({l,r});
}
merge(segs);
cout<<segs.size()<<endl;
return 0;
}
11.链表(数组模拟)
1.单链表
邻接表用来存储图和树
每个节点存两个数(数和next指针)
用两个数组表示邻接表
操作:
1.将x插入到头节点
2.在k后面添加节点
3.删除k后面的节点
邻接表就是多个单链表
代码
#include<iostream>
using namespace std;
const int N=100010;
//head 表示头节点的下标(指向下一个点)
//e[i] 表示节点i的值
//ne[i] 表示节点i的next指针是多少
//idx 存储当前已经用到了那个点,和数组顺序没关系,一直往后加就行了,相当于第几个加入的链表的
int head , e[N], ne[N] , idx;
//初始化
void init()
{
head =-1;
idx=0; //索引从0开始
}
// 将x插到头节点
void add_to_head(int x)
{
e[idx] = x;//添加
ne[idx]= head;//把添加的节点的next指针等于head的指针(指向下一个
head=idx;//让head指针指向添加的节点
idx++;
}
//将x插到下标是k的点后面
void add(int k,int x)
{
e[idx]=x;
ne[idx]=ne[k];
ne[k]=idx;
idx++;
}
//将下表是k的点后面的点删除
void remove(int k)
{
ne[k]=ne[ne[k]];// 让k节点的指针直接指向k后面的后面的节点//idx不用管
}
int main()
{
init();
int m;
cin>>m;
while(m--)
{
char op;
int k,x;
cin>>op;
if(op=='H')
{
cin>>x;
add_to_head(x);
}
if(op=='D')
{
cin>>k;
if(!k) head=ne[head];
else
{
remove(k-1);
}
}
if(op=='I')
{
cin>>k>>x;
add(k-1,x);
}
}
for(int i=head ;i!=-1;i=ne[i]) cout<<e[i]<<" ";
return 0;
}
2.双链表
有两个指针,一个前一个后
1.在k指针右边添加一个节点
2.删除
代码
#include<iostream>
using namespace std;
const int N=100010;
int e[N],l[N],r[N],idx;
//初始化
void init ()
{
//0表示左端点,1表示右端点
r[0]=1,l[1]=0;
idx=2;
}
//在下标是k的点的右边,插入x
void add(int k,int x)// 在左边插入 add(l[k],x);
{
e[idx]=x;
r[idx]=r[k];//让该节点的右指针指向k的右侧
l[idx]=k;//让该节点的左节点指向k
l[r[k]]=idx;//先让k先前的右侧节点的左指针指向该节点
r[k]=idx;//再让k的右指针指向该节点
}
//删除第k个点
void remove(int k)
{
r[l[k]]=r[k];//k的左节点的右指针指向k的右节点
l[r[k]]=l[k];//k右节点的左指针指向k的左节点
}
12.栈
先进后出
模拟栈代码
#include<iostream>
using namespace std;
const int N=100010;
int stk[N],tt;//栈的下标
//插入
stk[++tt] =x;
//弹出
tt--;
// 判断栈是否为空
if (tt>0) not empty ;
else empty;
//栈顶
skt[tt];
单调栈
基本上只考找离目标值左侧最近的且小于目标值的值
思路:
用栈实现
因为要找较小值,每次要入栈一个数,如果栈顶元素大于该数,直接删除(因为入栈值较小,一定不会用到删除的值)使栈单调递增
#include<iostream>
using namespace std;
const int N=100010;
int stk[N] ,tt ;
int main()
{
int m;
scanf("%d",&m); //cin,cout 可以用 ios::sync_with_stdio(false)加速,但是只能快几百毫秒,scanf比他们快将近十倍(在输入较大的情况下)
for(int i=0;i<m;i++)
{
int n;
scanf("%d",&n);
while(stk[tt]>=n&&tt!=0) tt--; //每个数最多出栈一次,进栈一次,最多2n次,时间复杂度是o(n);
if(!tt) printf("-1 ");
else printf("%d ",stk[tt]);
stk[++tt] = n;
}
return 0;
}
表达式求值(中缀表达式)
注意运算符优先级,且有括号
主要思想优先算优先级高的运算符,括号特判,优先计算
叶结点是数字,子结点是运算符,且优先级由高到低
#include<bits/stdc++.h>
using namespace std;
stack<int> num; //存数字
stack<char> op; //存运算符
map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}}; // 用map定义优先级
void eval() // 计算当前运算符所计算的值
{
int b=num.top();num.pop(); //注意a,b顺序因为-,/ 顺序不能颠倒
int a=num.top();num.pop(); //提取完就删除
int c;
auto x=op.top();
if(x=='+') c=a+b;
else if(x=='*') c=a*b;
else if(x=='-') c=a-b;
else c=a/b;
op.pop(); // 用完就删
num.push(c); // 添加计算完成的值
}
int main()
{
string str; // 读取表达式
cin>>str;
for(int i=0;i<str.size();i++) // 依次遍历表达式
{
auto c=str[i];
if(isdigit(c))
{
int j=i;
int x=0;
while(j<str.size()&&isdigit(str[j])) x=x*10+(str[j++]-'0'); // 提取数字(不一定是一位的,用循环提取去完整的数字)
i=j-1; //注意要把i的位置也要调整
num.push(x);
}
else if(c=='(') op.push(c); // 遇到( 存入栈里
else if(c==')')
{
while(op.top()!='('&&op.size()) // 开始整合括号里的值,如果运算符比较多,会在else里处理一部分
{
eval(); //从右向左计算
}
op.pop(); // 用完删去(
}
else
{
while(op.size()&&pr[c]<=pr[op.top()]) eval(); // 优先计算优先级高的运算符,如果栈顶大于则先算栈内的数
op.push(c); //必须是大于等于,因为同级运算符要从左向右计算,(-)(/)顺序不能颠倒
} // 同级优先计算左侧,这才能保证上面括号和下面收尾的计算正确
}
while(op.size()) eval(); // 提取剩余的表达式,从右向左计算(栈的性质)
cout<<num.top();
return 0;
}
后缀表达式
较为简单依次读取即可
13.队列
模拟队列
#include<iostream> using namespace std; const int N=100010; int q[N], hh,tt;//hh是队头,tt是队尾 //在队尾插入元素,在队头弹出元素 // 插入 q[++ tt] = x; //弹出 hh++; // 判断队列是否为空 if(hh<=tt) not empty; else empty; //取出队头元素 q[hh]
单调队列(滑动窗口)
基本上求滑动窗口的最大值和最小值
注意一点,队列存的是数组下标,这样方便限制窗口大小和移动窗口。
#include<iostream> using namespace std; const int N=1000010; int a[N],q[N]; // 因为是滑动窗口,需要一个数组当模板,队列当窗口,队列只能代表数组下标 int main() { int n,k; scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%d",&a[i]); int hh=0,tt=-1; // 初始化 for(int i=1;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) printf("%d ",a[q[hh]]); //因为实现了单调性,所以队头一定是最小的,即是答案。 } puts(""); //换行 hh=0,tt=-1; for(int i=1;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) printf("%d ",a[q[hh]]); } return 0; }
14.KMP
方法
问题: 求模板串(小)在模式串(大)中所有出现的位置的下标
暴力解法:两个for循环依次遍历 o(m*n)
kmp:
1.思路:寻找前缀和后缀最大的相等部分(前缀和后缀不能是全部串)
遇到不匹对(j+1与i比较)的情况让j回退为最长前缀后缀的长度,即模板串变成”ab“,若还不匹对,就一直回退,直到j=0,从头再对比,若还不匹对,i进位,比对下一位;
2.精髓:预处理模板串每一个下标存入next[]数组(存从0到当前下标模板串中最长相等前缀后缀长度);
和上方方法一样,运用递归,让模板串和模板串匹对,若成功存next[i]=j(j的长度就是当前下标最长前缀后缀的长度),不成功就回退,直到匹对成功或者j=0。(解释:第一条(1)当模板,用下一条(2)与其匹对,相当于(1)就是从0到i的后缀部分,(2)就是前缀部分,当匹对成功,j就是前缀的长度。
代码
#include<iostream> using namespace std; const int N=100010,M=1000010; int n,m; char q[N],a[M]; //模板串,和模式串 int ne[N]; // 精髓next数组,next被一些头文件用过,最好用ne以防报错 int main() { cin>>n>>q+1>>m>>a+1; // 输入 +1是数组从一开始读入,可以从0开始读入,思路一样 // 求next数组 for(int i=2,j=0;i<=n;i++) // 因为i=1时 ne[1]肯定等于0,而且下面的for会死循环,因为i和j都一样,每次匹对都成功 // ne[j]=j,j就无法回退。注意一点,前缀后缀最大不能是整个串,所以i=1时,没有前缀后缀。 { while(j&&q[i]!=q[j+1]) j=ne[j]; // 用递归,找合适的j值 if(q[i]==q[j+1]) j++; // 匹对成功,j进位,匹对下一位,或者之前的都删完了,找到能匹对的地方后,从头继续开始匹对 ne[i]=j; // 存入 } // kmp匹对过程 for(int i=1,j=0;i<=m;i++) // j从0开始是因为,每次要匹对j的下一位(j+1) { while(j&&a[i]!=q[j+1]) j=ne[j]; // 减少时间 if(a[i]==q[j+1]) j++; if(j==n) // 匹对完成 { cout<<i-n<<" "; // 因为字符串是从1开始读入,所以i-n+1-1。 j=ne[j]; // 匹对完成也要让j回退,寻找下一个 } } return 0; }
15.Trie树(字典树)
介绍
高效的存储和查找字符串集合的数据结构
存储的字符串个数不会很多
可以插入,查询,每存入一组结尾要标记
模拟Trie树
#include<iostream> using namespace std; const int N = 100010; int son[N][26],cnt[N],idx; // [26]因为最多有26个英语单词,最多有26个分支。cnt是有几个单词结束点,idx是下标,下标是0的点既是根节点,也是空节点 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; // idx是模拟数据结构的精髓,来区别每一个存入的数,防止重复,每条路径的idx都不一样,用以区分,表示当前的节点 // 路径存在就沿着走,不存在就创建 p=son[p][u]; // ,idx记录当前位子,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 n; cin>>n; while(n--) { char op[2]; // 字符串不吸收空格,遇见空格直接结束读入,这样防止读入空格 cin>>op>>str; if(op[0]=='I') insert(str); else cout<< query(str)<<endl; } return 0; }
最大异或对
先把所有的数存进树中,再根据需要找该数最大的异或对
#include<iostream> #include<cmath> #include<algorithm> using namespace std; const int N=3100010,M=100010; //有1e5个数,每个数的二进制有30位,所以N最少开3e6 int son[N][2],idx; // 只有0,1,就开两个分支 int a[M]; void insert(int x) { int p=0; //从根开始 for(int i=30;i>=0;i--) // 遍历每一个数的二进制位 { int &s=son[p][x>>i&1]; // &是取地址符,可以直接用来改数据 if(!s) s=++idx; //记录 p=s; // 记录 } } int query(int x) { int p=0,res=0; //从根开始 for(int i=30;i>=0;i--) { int s=x>>i&1; if(son[p][!s]) // 走相反的路径 { res+=1<<i; // 相反为1,再向左移i位 p=son[p][!s]; }else p=son[p][s]; } return res; } int main() { int n; cin>>n; for(int i=0;i<n;i++) { cin>>a[i]; insert(a[i]); } int m=0; for(int i=0;i<n;i++) { m=max(m,query(a[i])); } cout<<m; return 0; }
16.并查集
操作(可以维护额外信息)
1.将两个集合合并
2.询问两个元素是否在一个集合当中
基本原理:每个集合用一棵树表示。树根的编号就是整个集合的编号。每个节点存储他的父节点,p[x]表示父节点
问题1:如何判断树根:if(p[x]==x)
问题2:如何去求x的集合编号 :while(p[x]!=x)x=p[x]
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号,p[x]=y(树合并)(让一颗树变成另一个树的儿子)
优化:路径压缩,如果一个点能找到根节点,那么这一条路径都直接指向根节点
代码(无向图连通块中点的数量)(维护数量)
#include<iostream> using namespace std; const int N=100010; int n,m; int p[N],s[N];// s是集合中点的个数 int find(int x) //返回x的祖宗节点+路径压缩(直接让路上的点指向祖宗节点) { if(p[x]!=x) p[x]=find(p[x]);//递归,找到根节点的条件是p[x]==x; //递归里,如果不匹对,就递归,让p[x]==p[p[x]],向上找,直到成功,然后返回根节点, //回来时,所有p[x]一直==根节点实现了路径压缩。 return p[x]; } int main() { cin>>n>>m; for(int i=1;i<=n;i++) { p[i]=i; // 定义每个根节点 s[i]=1;// 初始化集合个数 } while(m--) { char op[4];// 字符串不吸收空格,遇见空格直接结束读入,这样防止读入空格 int a,b; cin>>op; if(op[0]=='C') { cin>>a>>b; if(find(a)==find(b)) continue; // 如果已经在一个集合里了就不用再重复加 s[find(b)]+=s[find(a)]; // 合并集合后也合并个数 p[find(a)]=find(b); } else if(op[1]=='1') { cin>>a>>b; if(find(a)==find(b)) puts("Yes"); else puts("No"); }else { cin>>a; cout<<s[find(a)]<<endl; } } return 0; }
食物链(维护与根节点的距离)
判断假话
相除余1,吃根节点
余2,吃1
余0,与根节点同类
#include<iostream> #include<cmath> using namespace std; const int N=100010; int p[N],d[N]; // p是父节点 d是离根的距离 int find(int x) { if(p[x]!=x) { int t=find(p[x]); // 递归找到根节点 d[x]+=d[p[x]]; // 当前距离d[x]+ 他的根节点到,根的距离 p[x]=t; // 再让路径上p[x] 都等于他根节点,路径压缩 } return p[x]; } int main() { int n,m; cin>>n>>m; for(int i=1;i<=n;i++) { p[i]=i; // 每一个数都是一个集合 ,自己都是自己的根节点 } int sum=0; for(int i=0;i<m;i++) { int o,a,b; cin>>o>>a>>b; if(a>n||b>n) sum++; else { int pa=find(a),pb=find(b); if(o==1) { if(pa==pb&&(d[a]-d[b])%3) sum++; // pa=pb说明他们都已经纳入到判断当中 // 就该判断是否是假话,当他们的距离相减不等于0,则就是假话 else if(pa!=pb) // 说明他们是第一次说,是真的 { p[pa]=pb; d[pa]=d[b]-d[a]; // 让a的根节点等于b后,让根节点到a的距离等于到b的距离 // 则 d[b] =d[a]+d[pa]; } }else if(o==2) { if(pa==pb && ((d[a]-d[b])-1)%3) sum++; // 即使相减为负数也不影响 // 例:2-4=-2,-2-1=-3,-3%3=0 else if(pa!=pb) { p[pa]=pb; d[pa]=d[b]-d[a]+1; // 因为a要吃b 所以a到根节点的距离比b的多1 } } } } cout<<sum; return 0; }
17.手写堆
介绍
是一颗完全二叉树(除了最后一层上面节点都是满的)
小根堆(小于等于左右儿子) 从下往上看,父节点都是最小值
存储:用一维数组存,x的左儿子是 2x,右儿子是2x+1(完全二叉树都是类似方法存储)
基本操作:down(x)
up(x)
实现操作:
1:插入一个数 heap[++size] = x; up(size); 在最后添加上数,根据位置一直up;
2:求集合当中的最小值 heap[1]
3:删除最小值 用整个堆的最后一个元素覆盖第一个,然后size--,删除最后一个节点,在把头元素down下来
heap[1]=heap[size]; size--;down(1);
4:删除任意一个元素 和删除头节类似
heap[k] =heap[size]; size--;down(k);up(k);
5:修改任意一个元素 heap[k] = x;down(k);up(x);
操作代码
#include<iostream> #include<algorithm> using namespace std; const int N=100010; int n,m,s; int h[N]; void down(int u) { int t=u; if(u*2<=s && h[u*2]<h[t]) t=u*2; // 如果u的左端点存在且比较小,替换他的下标 if(u*2+1<=s && h[u*2+1] < h[t]) t = u*2 +1; // 如果右端点存在且比较小,替换他的下标 if(u!=t) { swap(h[u],h[t]); // 取得最小值后,如果不是他本身,就开始交换 down(t); } } void up(int u) { while(u/2&&h[u/2]>h[u]) // 如果还存在父节点,而且父节点大于该节点就开始循环 { swap(h[u/2],h[u]); u/=2; } } int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>h[i]; s=n; for(int i=n/2;i;i--) down(i); // 排序,从第二次开始排,时间复杂度是o(n)(其他方法一般都是o(nlnn) // 原理:从第二层开始,n/2个数down1次,第三次n/4down2次·····就是n(1/2+2/2^2+3/2^3···)<n while(m--) // 持续输出最小值 { cout<<h[1]<<" "; h[1]=h[s]; s--; // 删除最小值; down(1); } return 0; }
模拟堆(维护第k个数,多开两个数组)
#include<iostream> #include<algorithm> #include<string.h> using namespace std; const int N=100010; int n,m; int h[N],hp[N],ph[N],s; //ph[k]=i,hp[i]=k, ph是第k个数的下标是多少,hp是下标是i,是第几个数 void heap_swap(int a,int b) { swap(ph[hp[a]],ph[hp[b]]); // 交换第k个数的下标 swap(hp[a],hp[b]) ; // 交换两个数的 k值 swap(h[a],h[b]); // 交换两个数; } void down(int u) { int t=u; if(u*2<=s && h[u*2]<h[t]) t=u*2; // 如果u的左端点存在且比较小,替换他的下标 if(u*2+1<=s && h[u*2+1] < h[t]) t = u*2 +1; // 如果右端点存在且比较小,替换他的下标 if(u!=t) { heap_swap(u,t); // 取得最小值后,如果不是他本身,就开始交换 down(t); } } void up(int u) { while(u/2&&h[u/2]>h[u]) // 如果还存在父节点,而且父节点大于该节点就开始循环 { heap_swap(u/2,u); u/=2; } } int main() { scanf("%d",&n); while(n--) { char op[10]; int k,x; scanf("%s",op); if(!strcmp(op,"I")) { scanf("%d",&x); s++; m++; ph[m]=s; hp[s]=m; h[s]=x; up(s); }else if(!strcmp(op,"PM")) { printf("%d \n",h[1]); }else if(!strcmp(op,"DM")) { heap_swap(1,s); s--; down(1); }else if(!strcmp(op,"D")) { scanf("%d",&k); k=ph[k]; heap_swap(k,s); s--; down(k); up(k); }else { scanf("%d %d",&k,&x); k=ph[k]; h[k]=x; down(k); up(k); } } return 0; }
18.哈希表
介绍
一般只有添加和查找
作用:把一堆比较大的数,映射到较小的(0~10^9----->0~10^5)
1.实现:哈希函数:一般直接取模,取模的数最好是质数
2.冲突:可能两个数映射成一个数
存储结构:1.开发寻址法
只开一个数组,开数据的2,3倍,找到k如果有·依次向后找直到插入
2.拉链法:
如果有冲突,用一个链存储。
字符串哈希方式:字符串前缀哈希法
先预处理出来所以前缀的哈希值
将前缀中每一个字母表示p进制上的每一位数 ,再取余q
不能映射成0,不用考虑冲突情况在p取131,13331和q取2^64,在99.99%情况下不会冲突
一般用unsigned long long 存,溢出就相当于取模2^64
模拟哈希表(拉链法)
#include<iostream> #include<cstring> using namespace std; const int N=100003; int h[N],e[N],ne[N],idx; // h是哈希表,e是链表存的值,ne是指针指向下一个节点 void insert(int x) { int k=(x%N+N)%N; // 取余再相加,防止出现负数 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() { memset(h,-1,sizeof(h)); int n; scanf("%d",&n); while(n--) { char op[2]; int x; scanf("%s%d",op,&x); if(op[0]=='I') { insert(x); }else { if(find(x)) puts("Yes"); else puts("No"); } } return 0; }
开放寻址法
#include<iostream> #include<cstring> using namespace std; const int N=200003, null = 0x3f3f3f3f; // 一个稍微大于1e9的数,表示无穷大,又保证相加不溢出 int h[N]; int find(int x) // 如果不存在,返回能存的位置,如果存在返回下标 { int k=(x%N+N)%N; while(h[k]!=null&&h[k]!=x) { k++; if(k==N) k=0; } return k; } int main() { memset(h,0x3f,sizeof(h)); // 中间是赋的值,根据数组的字节来赋值,会把int的四位依次变成该值,所以0x3f变成0x3f3f3f3f // 慎用,一般只用 0,-1,0x3f int n; scanf("%d",&n); while(n--) { int x; char op[2]; scanf("%s%d",op,&x); if(op[0]=='I') { h[find(x)] =x; }else { if(h[find(x)]==x) puts("Yes"); else puts("No"); } } return 0; }
前缀和哈希(p进制)()
能用kmp的都可以用这个
#include<iostream> using namespace std; const int N=100010; typedef unsigned long long ULL; // 溢出相当于取模2^64了 int P=131; char str[N]; ULL h[N],p[N]; // 因为在求中间字符串的值的时候,要把第一个字符串扩大p^a-b+1 倍,所以预处理p数组 ULL get(int a,int b) // 获取中间字符串的哈希值 { ULL sum; sum=h[b]-h[a-1]*p[b-a+1]; return sum; } int main() { int n,m; cin>>n>>m>>str+1; p[0]=1; for(int i=1;i<=n;i++) { p[i]=p[i-1]*P; h[i]=h[i-1]*P+str[i]; // str只要不为0都可以 } 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; }
19.STL
vector
,变长数组,倍增的思想,操作和string相似
size() 返回个数
empty ()返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end() 迭代器
[]
支持比较运算
pair
pair<int,int> p
first,第一个元素
second 第二个元素
支持比较运算,以first为第一关键字,以second 为第二关键字(字典序)
写:p=make_pair(10,"ccc);
p={20,"ccc} //c++.11;
可以嵌套
pair<int,pair<int,int> p;
string
,字符串,
size()/length()
empty()
clear()
1.截取子字符串:substr(参数1,参数2)参数1是起始下标,可以是0,正整数,负数(表示倒数第几个),参数2是截取个数,如果是0,负数则返回空字符串
2.返回首地址:c_str() ,该函数是将string转化为c的字符串数组,生成一个const char*的指针且可读不可改,指向字符串的首地址
例:char*p=s[10];
string a="ccc";
strcpy(p,a.c_str()); // 一定用strcpy,因为一个数指针,一个是常指针,不赋值
cout<<p; //输出为ccc
3.可以用关系运算符,可以=,+;
4.查找函数:find(参数1,参数2) 参数1是要找的目标字符串,参数2是开始查找的起始位置,不写默认从0开始。找到返回第一次出现的下标,没有返回-1; 还有find_first_of(),从头开始找第一个出现的,find_last_of(),从后面开始找最后一个,rfind()从后开始找找最后一个,find_first_not_of(),找第一个不匹对的字符
5.插入 insert(起始位置,插入的字符串),删除erase(起始位置,终止位置)用迭代器表示,或者erase(起始位置,删除个数)用正常的下标
queue
队列
1.往队尾插入 push(),返回队头,front(),返回队尾back() 队头弹出,pop()
没有clear函数,重新构造就行了
q=queue<int> ();
priority_queue (默认大根堆)
优先队列,
没有clear
改成小根堆 heap.push(-x) ,把数改成负数,
或者改定义:priority_queue<int,vector<int>,greater<int>> heap;
插入push(),返回堆顶,top(),堆顶弹出,pop();
stack
栈
size()
empty()
push()插入栈顶元素,top(),返回栈顶元素,pop(),删除栈顶元素
没有clear
deque
双端队列
加强版的vector
功能多,但是速度太慢
size()
empty()
clear()
front()/back()
push_front()/pop_front()
push_back()/pop_back;
begin()/end()
[]
set map multiset multimap
multi是多,multiset和multimap可以存重复元素
基于平衡二叉树(红黑树)实现
动态维护有序数列
size()
empty()
clear()
set/multiset
insert()
find()
count() 返回某一个数的个数
begin()/end() 可以++--;
erase()
1.输入是一个数,则 删除所有x o(k+logn)
2.输入迭代器,删除这个迭代器
lower_bound()/upper_bound() lower_bound(x) 返回大于等于x的最小数的迭代器, 不存在返回end()
upper_bound(x) 返回大于x的最小值的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair 或者迭代器
find() o(logn)
[]
lower_bound()/upper_bound()
unordered_set unordered_map unordered_multiset ordered_multimap
无序的,基于哈希表实现
和上面类似,增删改查的时间复杂度的o(1)
不支持lower_bound()/upper_bound() ,迭代器的++--,所有和排序的操作都不支持
bitset
可以压缩空间
压位
bitset<10000> s;
~s, | & ^ >> << == != []
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0】
set() 把所有位置成1
set(k,v) 将第k位变成v
reset() 把所有位变成0;
flip ()等价于 ~
flip(k) 把第k位取反
20.DFS(深度优先搜索)
介绍
尽可能往深的搜,搜到头就会回溯返回上一个节点,看看能不能继续向下搜,回溯要恢复路径
数据结构:stack栈
空间o(n)
不具有最短路的特点
剪枝:感觉不如最优解,或者一定不合法,就把这条路剪掉不再往下走
经典例题:全排列
#include<iostream> using namespace std; const int N=10; int n; int path[N],st[N]; void dfs(int u) { if(u==n) { for(int i=0;i<n;i++) cout<<path[i]<<" "; cout<<endl; return ; } for(int i=1;i<=n;i++) { if(!st[i]) { path[u]=i; // 给该数赋值 st[i]=true; // 标记为用过 dfs(u+1); // 继续往下搜 st[i]=false ;// 恢复路径 } } } int main() { cin>>n; dfs(0); }
n—皇后问题
第一种方法(按行枚举) 类似于树,每个节点的有分叉,把分叉走完再向上回溯
时间复杂度 o(n!)
#include<iostream> using namespace std; const int N=20; char a[N][N]; bool lie[N],dui[N],udui[N]; // 列,对角线,反对角线 int n; void dfs(int u) // 每一层遍历 { if(u==n) { for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { cout<<a[i][j]; } cout<<endl; } cout<<endl; } for(int i=0;i<n;i++) // 每一行,从头找 { if(!lie[i]&&!dui[u-i+n]&&!udui[u+i]) // 每一个对角线都有自己的截距 // u=i+d,u=-i+d ,则d1=u-i,但是可能为负数,所以整体上移,加上n,d2=u+i { a[u][i]='Q'; lie[i]=dui[u-i+n]=udui[u+i]=true; dfs(u+1); lie[i]=dui[u-i+n]=udui[u+i]=false; a[u][i]='.'; } } } int main() { cin>>n; for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { a[i][j]='.'; } } dfs(0); }
第二种方法(按每个元素枚举) 类似于九连环,一直递归到最后一个元素,然后回溯找答案
时间复杂度是 o(2^(n^2)) 每个位置有两个情况,一共有n^2个位置
#include <iostream> using namespace std; const int N = 20; int n; char g[N][N]; bool row[N], col[N], dg[N], udg[N]; // 因为是一个个搜索,所以加了row // s表示已经放上去的皇后个数 void dfs(int x, int y, int s) { // 处理超出边界的情况 if (y == n) y = 0, x ++ ; if (x == n) { // x==n说明已经枚举完n^2个位置了 if (s == n) { // s==n说 明成功放上去了n个皇后 for (int i = 0; i < n; i ++ )puts(g[i]); puts(""); } return; // 当x=n时,已经超过矩阵范围了,一定要终止递归 } // 分支1:放皇后 没有先后关系 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); row[x] = col[y] = dg[x + y] = udg[x - y + n] = false; g[x][y] = '.'; } // 分支2:不放皇后 dfs(x, y + 1, s); } int main() { cin >> n; for (int i = 0; i < n; i ++ ) for (int j = 0; j < n; j ++ ) g[i][j] = '.'; dfs(0, 0, 0); return 0; }
21.BFS(宽度优先搜索)
介绍
一层一层的搜,把每一层都搜完
数据结构:queue;
空间:o(2^n)
因为是一层一层搜,具有最短路的特点
代码(走迷宫)
当有障碍物无法到达终点时,不要让模拟队列的队尾超出合法点的个数,因为队头也需要多加1,t取到(0,0)了,可能终点就在附近而判错。
/* 思路:找0上下左右可行的情况,开辟分支,每一个分支都进行搜索,肯定会有一条分支找到终点,且是最短的路径 */ #include<iostream> #include<cstring> using namespace std; typedef pair<int,int> PII; const int N=101; int a[N][N],d[N][N]; // a是存图 ,d是存该点到起点的距离 PII q[N*N]; // 用队列存符合条件的坐标(模拟的) 也可以用stl queue<PII> q; int n,m; int bfs() { int hh=0,tt=0; // 初始化 q[0]={0,0}; // 起点 memset(d,-1,sizeof d); // 初始化,而且标记为为使用 d[0][0]=0; // 初始起点的距离(移动次数) while(hh<=tt) { auto t=q[hh++]; // 用t 存正在判断的坐标; int dx[4]={0,1,0,-1},dy[4]={-1,0,1,0}; // 四个点依次组合起来介绍上,右,下,左的坐标 for(int i=0;i<4;i++) { int x=t.first+dx[i],y=t.second+dy[i]; // 获取下一个点的坐标 if(x>=0&&x<n&&y>=0&&y<m&&a[x][y]==0&&d[x][y]==-1) // 判断 { d[x][y]=d[t.first][t.second]+1; // 符合条件就存上该点到起点的距离(移动次数) q[++tt] = {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>>a[i][j]; } } cout<<bfs(); return 0; }
代码(八码数、、数字华容道)
/* 思路:用字典记录交换次数和交换后的字符串,保证不重复交换,让x与上下左右都交换,四个交换后再与x的上下左右交换,枚举记录每一种情况,绝对会有一条分支找到答案且第一个跳出来 */ #include<iostream> #include<unordered_map> #include<queue> using namespace std; unordered_map<string,int> d; // 用字典存储变化的次数和变化后的字符串 queue<string> q; // bfs的队列 string a; // 存字符串 int bfs(string start) { string end = "12345678x"; // 结束判定条件 q.push(start); d[start] = 0; // 初始化次数 int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1}; // 上下左右的坐标 while(q.size()) { auto t = q.front(); q.pop(); int distence = d[t]; if(t == end) return distence; // 成功判定条件 int s = t.find('x'); // 找到x所在字符串的位置 for(int i = 0; i < 4; i ++) { int x = s / 3 + dx[i],y = s % 3 + dy[i]; // 上下左右的坐标 if(x >= 0 && x < 3 && y >= 0 && y <3) // 判断是否出界 { swap(t[s],t[x * 3 + y]); // 与上下左右交换 if(!d.count(t)) // 如果交换后的结果之前没有,则记录 { d[t] = distence + 1; q.push(t); } swap(t[s],t[x * 3 + y]); // 在变回去,因为for循环没结束还要用t } } } return -1; // 如果循环结束还没有答案,返回-1; } int main() { for(int i=0;i<9;i++) { char x; cin>>x; a+=x; } cout<<bfs(a); return 0; }
22.树与图的深度,宽度优先遍历
介绍
有向图可以用邻接矩阵和邻接表表示
邻接矩阵就是用0,1表示,但是占用空间较大适合数据间隔交稠密的例子,一般不用
邻接表是多个单链表,每个链表存该节点和其他节点连接
树的深度优先遍历,找一个节点然后不断向下遍历,到头再回溯
代码(树的深度优先遍历)
#include<iostream> #include<cstring> using namespace std; const int N=100010,M=2*N; // 因为是无向图,两个点之间有两条线,要用两倍的数组存 int h[N],e[M],ne[M],idx; // 链表头 , 链表存的节点 , 下一个位置指针, bool st[M]; // 标记,因为是无向图,标记用过的,不能再用了,防止往回走 int ans=N; // 最小的最大值 int n,a,b; int dfs(int u) // 函数每次返回该节点总分支节点和自己的个数 { int sum = 1, res = 0; // sum是分支带上该节点的个数 st[u]= true; // 标记,不再使用,保证树往下遍历 for(int i=h[u];i != -1; i = ne[i]) // 从该节点出发,依次到和该节点有关的节点,同理往下走 { int j=e[i]; if(! st[j]) { int s=dfs(j); res = max(s , res); // 找最大分支(最大连通块) sum+=s; // 计算分支个数 } } res=max(res,n-sum); // 比较最大分支和上方的大小 ans=min(ans,res); // 找最小的最大值 return sum; } void add(int a,int b) { e[idx]=b,ne[idx] = h[a], h[a] = idx++ ; } int main() { memset(h,-1,sizeof h); cin>>n; for(int i=0;i<n-1;i++) { cin>>a>>b; add(a,b); add(b,a); } dfs(1); cout<<ans; return 0; }
树的宽度优先遍历,按照和初始节点的距离,按层遍历
代码(图的宽度优先遍历)
#include<iostream> #include <queue> #include<cstring> using namespace std; const int N=100010; int n,m; int h[N],ne[N],e[N],d[N],idx; // 邻接表,d是表示到1的距离以及标记是否用过 void add(int a, int b) // 邻接表头插法模板 { e[idx]=b,ne[idx]=h[a],h[a]=idx++; } queue<int> a; // 宽搜,必不可少的队列 int bfs(int u) { memset(d,-1,sizeof d); // 初始化距离为-1 d[u]=0; // 初始1的距离 a.push(u); while(a.size()) { auto t=a.front(); a.pop(); for (int i=h[t];i!=-1;i=ne[i] ) // 找该节点的下方节点 { int j=e[i]; if(d[j]==-1) { d[j]=d[t]+1; // 标记该节点到1的最短距离 if(j==n) return d[n]; // 找到就直接返回 a.push(j); } } } return -1; // 没找到就返回-1 } int main() { memset(h, -1, sizeof h); cin>>n>>m; for(int i=0;i<m;i++) { int a,b; cin>>a>>b; add(a,b); } if(n==1) cout<<"0"; else { int p=bfs(1); cout<<p; } return 0; }
23.拓扑序列
介绍
拓扑序列:起点在终点前面,所有边但是从前指向后
有向无环图也叫拓扑图
度数: 一个点有几个边进来就是入度,有几个出去就是出度
入度为0可以当作起点(没有一个点在它前面)
方法:把所有入度为0的点存进队列,t是队头,枚举t的所有出边,依次删除该边马,如果该点的入度为零,入队,如果该图有环,那么所有点都不会入队,如果无环,一定全部入队(一定存在一个入度为零的点)
代码
#include<iostream> #include<queue> #include<cstring> using namespace std; const int N=100010; int h[N],ne[N],e[N],idx,q[N],d[N]; // 邻接表,队列,入度。 int n,m; void add(int a,int b) { e[idx] = b,ne[idx] = h[a], h[a]=idx++; d[b]++; } int topsort() { int hh = 0,tt = 0; // 初始化,tt初始为-1也可以 for(int i=1;i<=n;i++) { if(!d[i]) q[tt++] = i; // 找入度为0的点,存入队列 } while(hh<=tt) { int t = q[hh++]; // 在循环中,hh一定会大于tt for(int i=h[t];i!=-1;i=ne[i]) { int j=e[i]; // 删除该边 d[j]--; if(!d[j]) q[tt++] = j; } } return tt==n; } int main() { memset(h,-1,sizeof h); cin>>n>>m; while (m -- ){ int a,b; cin>>a>>b; add(a, b); } if(topsort()) { for(int i=0;i<n;i++) cout<<q[i]<<' '; }else cout<<"-1"; return 0; }
24.最短路
介绍
1,单源最短路,求一个点到其他点的问题
1,所有边权都是正数
1,朴素Dijkstra算法 o(n*n) n表示点的数量,适合稠密图,和边数没关系 (m是n * n级别是稠密)
2,堆优化版的Dijkstra算法 o(mlogn)点和边在一个数量级
最短路: 2,存在负权边
1,Bellman-Ford 算法O(nm) (有不超过k条边适用)
2,SPFA算法,一般是O(m),最坏O(nm)
2,多源汇最短路,源点=起点,汇点=终点,会有许多询问,起点和终点不确定 1,Floyd 算法 O(n^3)
朴素版的Dojkstra算法
1.初始化距离,1.dist[1] = 0,dist[i] = 很大的数(除了起点,其他距离不知道)
s : 当前已确定最短距离的点存入集合s
2迭代循环 for i:0~n
不在s中的,距离最近的点t,加入到s,用t更新其他点的距离,如果该点到起点的距离大于t,则更新。
稠密图用邻接表矩阵
稀疏图用邻接表
代码(稠密图)
#include<iostream> #include<cstring> using namespace std; const int N=505; int g[N][N]; // 是稠密图,用邻接矩阵 int dist[N]; // 表示每个点到1的距离 int n,m,k,x,y,z; bool st[N]; // 判断有没有被用为最短的点 int dijkstra() // 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1 { memset(dist, 0x3f, sizeof dist); // 初始化距离 dist[1] = 0; for(int i=0;i<n;i++) // n个点,循环n次 { int t=-1; // 每次初始t=-1,来取得未被选中的点中第一个点 for(int j=1;j<=n;j++) { if(!st[j]&&(t==-1||dist[t]>dist[j])) // 如果是第一个点或者该点比t点距离还要小,则取得 // 在位被选中的点中,找最短的点 t=j; } st[t]=true; // 找到最短的点后,将他标记为用过 for(int j=1;j<=n;j++) { dist[j]=min(dist[j],dist[t]+g[t][j]); // 重新更新每个点的距离 } } if(dist[n]==0x3f3f3f3f) return -1; return dist[n]; } int main() { memset(g,0x3f,sizeof g); scanf("%d%d", &n, &m); for(int i=0;i<m;i++) {scanf("%d%d%d", &x,&y,&z); g[x][y]=min(g[x][y],z); } int x=dijkstra(); printf("%d",x); }
堆优化版的dijkstra算法
可以用手写堆,可以直接修改数,保证个数为n,
可以用优先队列,不能直接修改数,把每次的数插进队列,个数可能为m(边)
dijkstra是先初始化dist,循环是先找第一个点,所以第一次必找原点,根据原点和其他点的权更新和其有关点的dist,再循环根据dist找离原点最近的点,用该点更新dist,再用dist找最近的点
代码(稀疏图)
#include<iostream> #include<cstring> #include <queue> #include<algorithm> using namespace std; typedef pair<int, int> PII; const int N=1e6; int ne[N],h[N],idx,e[N]; // 链表 int w[N]; // 链表权值 int dist[N]; bool st[N]; int x,y,z,n,m; int dijkstra() { memset(dist, 0x3f, sizeof(dist)); dist[1] = 0; priority_queue<PII, vector<PII>, greater<PII>> heap; // 定义一个小根堆 // 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时 // 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。 heap.push({ 0, 1 }); // 这个顺序不能倒,pair排序时是先根据first,再根据second,这里显然要根据距离排序 while(heap.size()) { auto k = heap.top(); // 取不在集合S中距离最短的点 heap.pop(); int ver = k.second, distance = k.first; if(st[ver]) continue; st[ver] = true; for(int i = h[ver]; i != -1; i = ne[i]) { int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。 if(dist[j] > distance + w[i]) { dist[j] = distance + w[i]; heap.push({ dist[j], j }); } } } if(dist[n] == 0x3f3f3f3f) return -1; else return dist[n]; } void add(int x,int y,int z) { e[idx]=y,w[idx]=z,ne[idx]=h[x],h[x]=idx++; } int main() { memset(h,-1,sizeof h); scanf("%d%d",&n,&m); while(m--) { scanf("%d%d%d",&x,&y,&z); // 不用担心重边,在算法里,会取最短的值 add(x,y,z); } int j=dijkstra(); printf("%d",j); }
Bellman—Ford算法
两重循环,迭代n次,每次更新每条边
bellman-Ford算法 初始化dist后,按层找,先找和原点有关的点只更新他们的dist(其实是根据全部的边更新全部的点,但是开始只有原点是0,其他都是无穷大相当于没关系)再根据找到的点更新和他们有关的点。用backup数组是因为如果只有dist数组,更新的太多了可能会改变本身值,用backup数组记录更新前的dist保证更新时,本身点不会变。
代码(有边数限制和负权的最短路)
#include<iostream> #include<cstring> using namespace std; const int N=505,M=10010; // 点和边的数量 int dist[N],backup[N]; // 距离和上一次变化的距离 int n,m,k; struct // 用结构体存点,边,权 { int x,y,z; }edge[M]; int Bellman_ford() // 和bfs类似,每次只把和当前点有关的点改变距离,其他点维持无穷大,不能把点更新的太快 { memset(dist,0x3f,sizeof dist); dist[1]=0; for(int i=0;i<k;i++) { memcpy(backup,dist,sizeof dist); // 每次用backup保存上一次的距离情况 for(int j=0;j<m;j++) { int a=edge[j].x,b=edge[j].y,c=edge[j].z; dist[b]=min(dist[b],backup[a]+c); } } if(dist[n]>0x3f3f3f3f/2) return 0x3f3f3f3f; // 因为有负权边,可能该点比无穷大小 return dist[n]; } int main() { scanf("%d%d%d",&n,&m,&k); for(int i=0;i<m;i++) { int a,b,c; scanf("%d%d%d",&a,&b,&c); edge[i]={a,b,c}; } int t=Bellman_ford(); if(t==0x3f3f3f3f) puts("impossible"); else printf("%d",t); return 0; }
spfa算法
根据bellman-ford优化,和dijkstra算法相似
在Bellman—ford算法上做优化,将每次变小的点存入队列,只要队列不空就循环
Bellman-Ford算法是每个点,每个路都会走,不管大小,所以使用backup数组,防止正在更新时,该点变小,更新完成后再变小不影响这条路经的dist。所以可以限制边的个数和存在负环
spfa算法虽然也是按层更新,但是不使用backup数组,在队列的点会变小,意味着放弃了原来的路径而走使其变小的路径,不利于限制边数,和dijkstra算法相似,只走更近的路,而且不能有负环。
代码(无负环的负权最短路)
#include<iostream> #include<cstring> #include<queue> using namespace std; const int N=1e5+10; int e[N],h[N],ne[N],idx; // 链表 int w[N]; // 权值 int dist[N]; // 距离 int n,m; bool st[N]; // 判断是否在队列 queue<int> heap; void add(int x,int y,int z) { e[idx]=y,w[idx]=z,ne[idx]=h[x],h[x]=idx++; } int spfa() // 用队列存更新后比原来小的点,每次遍历与其有关的点,也是按层更新 // 在队列里的点,如果该点更新的更小,会直接改变 //每次循环会消除标记,因为可能后面会使该点的值更小,可以再次使用 { memset(dist,0x3f,sizeof dist); dist[1]=0; heap.push(1); st[1]=true; while(heap.size()) { auto t=heap.front(); heap.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]) { st[j]=true; heap.push(j); } } } } return dist[n]; } int main() { memset(h,-1,sizeof h); scanf("%d%d",&n,&m); while(m--) { int a,b,c; scanf("%d%d%d",&a,&b,&c); add(a,b,c); } int t=spfa(); if(t==0x3f3f3f3f) puts("impossible"); else printf("%d",t); return 0; }
代码(判断负环)
#include<iostream> #include<cstring> #include<queue> using namespace std; const int N=10010; int ne[N],e[N],h[N],idx; int w[N]; int dist[N],cnt[N]; int n,m; bool st[N]; queue<int> q; void add(int x,int y,int z) { e[idx]=y,w[idx]=z,ne[idx]=h[x],h[x]=idx++; } bool spfa() // 在开一个cnt数组,用来存路径的边数,如果边数大于等于n,说明点数大于n+1,从而判断。 { // 不用初始化dist数组,因为如果有负环,dist一样会更新,一样会无限循环,从而判断 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(!st[j]) { st[j]=true; q.push(j); } if(cnt[j]>=n) return true; } } } return false; } int main() { memset(h,-1,sizeof h); scanf("%d%d",&n,&m); 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; }
Floyd算法
三重循环直接套
建立邻接矩阵,用Floyd算法将其变成两个点的最短距离
代码(多源最短路)
#include<iostream> #include<cstring> using namespace std; const int N=210,INF=1e9; int d[N][N]; // 邻接矩阵 int n,m,q; int 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); for(int i=1;i<=n;i++) // 初始化,除了相同的点是0,其他都是无穷 { for(int j=1;j<=n;j++) { if(i==j) d[i][j]=0; else d[i][j]=INF; } } while(m--) { int a,b,c; scanf("%d%d%d",&a,&b,&c); d[a][b]=min(d[a][b],c); // 排除重边 } Floyd(); while(q--) { int a,b; scanf("%d%d",&a,&b); if(d[a][b]>INF/2) puts("impossible"); else printf("%d\n",d[a][b]); } return 0; }
25.最小生成树
朴素版的Prim算法适合稠密图,堆优化版适合稀疏图,克鲁斯卡尔算法也是适合稀疏图,所以堆优化不常用
朴素版Prim
点到集合的距离是集合外的点到集合中点的最短的距离
#include<iostream> #include<cstring> using namespace std; const int N=510,INF=0x3f3f3f3f; // 定义无穷 int n,m; int g[N][N]; 邻接矩阵 int dist[N]; int st[N]; int Prim() // 和dijkstra模式一样,唯一区别是,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; } 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",t); return 0; }
堆优化和dijkstra一样
Kruskal算法
第二步用并查集实现
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N=2e5+10; struct Edge // 存边和权 { int a,b,w; bool operator< (const Edge &W) const { return w<W.w; } }edge[N]; int p[N]; // 并查集 int n,m; int find(int x) // 并查集的精髓 { if(p[x]!=x) p[x]=find(p[x]); return p[x]; } int Kruskal() { for(int i=1;i<=m;i++) p[i]=i; sort(edge,edge+m); // 排序后,重边自动就取最小的值 int res=0,cnt=0; // 权重和合并次数 for(int i=0;i<m;i++) { int a=edge[i].a,b=edge[i].b,w=edge[i].w; a=find(a),b=find(b); if(a!=b) // 排除自环,一开始每个点都是一个集合,要合并n-1次 { p[a]=b; res+=w; cnt++; } } if(cnt<n-1) puts("impossible"); else printf("%d",res); return 0; } int main() { scanf("%d%d",&n,&m); for(int i=0;i<m;i++) { int a,b,c; scanf("%d%d%d",&a,&b,&c); edge[i]={a,b,c}; } Kruskal(); return 0; }
26.二分图
一个图是二分图,则当且仅当图中不含奇数环
二分图是两个集合,集合内没有连线,两个集合之间有连接
染色法
判断图是否二分图,根据二分图不能有奇数环,依次染色,如果一个点染两个颜色,则矛盾
#include<iostream> #include<cstring> using namespace std; const int N=2e5+10; int n,m; int e[N],ne[N],h[N],idx; int color[N]; void add(int a,int b) { e[idx]=b,ne[idx]=h[a],h[a]=idx++; } bool dfs(int x,int c) // 用来判断该点和其以后的点是否符合二分图 { color[x]=c; for(int i=h[x];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() { scanf("%d%d",&n,&m); memset(h,-1,sizeof h); for(int i=0;i<m;i++) { 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]) // 如果没有染色,使用dfs走一遍 { if(!dfs(i,1)) { flag=false; break; } } } if(flag) puts("Yes"); else puts("No"); return 0; }
匈牙利算法
求最大的成功匹配数量,成功匹配是指不存在两条边共用一个点
为二分图,但是只需从一个集合出发,依次找每个点,如果后面的点没有匹配的点,看看前面和其共用的点能不能再找到一个点
#include<iostream> #include<cstring> using namespace std; const int M=1e5+10,N=510; int ne[M],h[N],e[M],idx; int n1,n2,m; int match[N]; // 存该点对应的点 bool st[N]; // 表示是否被考虑 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]) // 如果该点没有被x点考虑过 { st[j]=true; if(match[j]==0||find(match[j])) // 如果该点被匹配过,用find找其对应的点能不能再找到一个点 { match[j]=x; return true; } } } return false; } int main() { memset(h,-1,sizeof h); scanf("%d%d%d",&n1,&n2,&m); while(m--) { 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); // 每次判断一个点时,先把st数组初始化,重新开始考虑 if(find(i)) res++; } printf("%d",res); return 0; }
质数筛法
质数定理:1~n质数的个数是n/ln n(一般偏小)
试除法
最简单的判断,耗时较长
#include<iostream> using namespace std; bool isprimes(int n) { if(n==1) retrun false; else { for(int i=2,i<=n/i;i++) { if(n%i==0) return false; } } return true; }
埃氏筛
去除素数的倍数
数据大于1e7时变慢
#include<iostream> using namespace std; const int N=100010; int n; bool status[N] ;//标记是不是素数 // 0,1的标记也是0,有时候要特意排除 int primes[N];//素数数组 int cnt=0;// 代表当前素数的个数 bool get_primes(int n) // o(nlnlnn) { for(int i=2;i<=n;i++) { if(!status[i]) { primes[cnt++] = i // 没有标记的就是素数 for(int j=i*i;j<=n;j+=i) status[j] = 1; // 从i*i开始 } } }
欧拉筛
保证是最小质因数筛掉
#include<iostream> using namespace std; const int N=100010; int n; bool status[N] ;//标记是不是素数 // 0,1的标记也是0,有时候要特意排除 int primes[N];//素数数组 int cnt=0;// 代表当前素数的个数 bool get_primes(int n) // o(nlnlnn) { for(int i=2;i<=n;i++) { if(!status[i]) primes[cnt++] = i // 没有标记的就是素数 for(int j=0;primes[j]<=n/i;j++) { status[primes[j]*i] = ture; if(i%primes[j]==0) break;// 如果i是该质数的倍数或本身,那么直接跳出,后面的质数不用重复去乘,保证让最小的质数去乘, } } }
背包问题
物品有价值,有大小,用一个有空间的背包装
dp优化是对代码或者方程的等价变换
01背包问题
每件物品最多只用一次
一个有空间的背包装
Dp可以分为状态表示和状态计算
状态表示(二维f[i] [j])有属性(是求最大的值,还是求最小的值,还是求数量),和集合的本身(本身表示所有选法,集合的条件是只能从前i个物品中选,总体积小于等于j)每个点表示该选法的最大值
状态计算要先把集合划分,一个点有两个选法,一个是不含i,即f[i-1] [j],从1~i-1找最大值,第二个是含i,那就先减去i的空间和价值
为,f[i-1] [j-vi]+wi,两个选法求最大值,即为最优解
// 二维 #include<iostream> #include<algorithm> using namespace std; const int N=1010; int v[N],w[N]; // 体积和权值 int f[N][N]; // 集合 int n,m; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&w[i]); for(int i=1;i<=n;i++) // 因为当i=0时,数组已经是0,直接从1开始遍历 for(int j=0;j<=m;j++) { f[i][j]=f[i-1][j]; if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]); } printf("%d",f[n][m]); return 0; } // 一维 #include<iostream> #include<algorithm> using namespace std; const int N=1010; int v[N],w[N]; // 体积和权值 int f[N]; // 集合 int n,m; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&w[i]); // 该数组是依次向后递推的,用前一个数更新后一个数 // 所以可以不记录之前的数值,一直往下更新,是滚动数组 // 如果j从小到大,因为(j-v[i])<= j ,可能f[j-v[i]] 大于f[j] 然后把f[j] 更新 // 但是,f[j-v[i]] 是i-1的结果,如果再第i层把他更新了,会出现错误,所以从大到小 // 可以避免在i层把之前的结果更新 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]); printf("%d",f[m]); return 0; }
完全背包问题
每件物品有无限个
和前面那个相似,只是状态计算有一些不同
每个点列举每个方法取最优解,然后下一个点再用这个最优解和该点的每个方法的结果比较取最优解,保证答案是优中取优
每组物品看选几个
方程优化
#include<iostream> using namespace std; const int N=1010; int v[N],w[N]; int f[N][N]; int n,m; int main() { scanf("%d%d",&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++) { f[i][j]=f[i-1][j]; if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-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++) f[i][j]=max(f[i][j],f[i-1][j-k*v[i]+k*w[i]); 但是 f[i][j] =max(f[i][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,f[i-1][j-3v]+3w````) f[i][j-v]=max( f[i][j-v], f[i-1][j-2*v]+w,f[i-1][j-3v]+2w```` ) 所以 f[i][j]=max(f[i][j],f[i][j-v]+w); */ printf("%d",f[n][m]); return 0; } // 一维 #include<iostream> using namespace std; const int N=1010; int v[N],w[N]; int f[N]; int n,m; int main() { scanf("%d%d",&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++) { f[j]=max(f[j],f[j-v[i]]+w[i]); } // 因为原式为f[i][j]=max(f[i-1][j],f[i][j-v[i]) 第二个就是第i层的,所以j从小到大 printf("%d",f[m]); return 0; }
多重背包问题
每个物品的个数不一样
和完全背包问题方法相似
#include<iostream> #include<algorithm> using namespace std; const int N=110; int n,m; int v[N],w[N],s[N]; int f[N][N]; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d%d%d",&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*v[i]<=j;k++) f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]); printf("%d",f[n][m]); return 0; }
优化
因为个数太大,会超时,将s拆分为2的若干次幂
比如 1023拆为 1,2,4,8,16,32,64,128,256,512(1和2能凑0~3,加上4能凑0~7,加上8能凑0~15,。。。。)
上面的数可以凑0~1023.
比如200 拆为 1,2,4,8,16,32,64,73;
因为前面能凑0~127,加上73,能凑200;
用这个二进制优化法,可以把大的一个物品,分为多个物品,(可以选取其中任意数量个)
然后转化为01背包问题看那个物品可以选(相当于选每个物品中最优的个数)
#include<iostream> #include<algorithm> using namespace std; const int N=12000; // 有1000个物品,每个物品最多有1000个可以分成12组,所以取12000 int n,m; int v[N],w[N]; int f[N]; int main() { scanf("%d%d",&n,&m); int cnt=0; for(int i=1;i<=n;i++) { int a,b,c; // 物品的体积,权值,个数。 scanf("%d%d%d",&a,&b,&c); int k=1; while(k<=c) // 开始分组,分成若干二次幂 { cnt++; v[cnt]=k*a; // cnt会一直计数 w[cnt]=k*b; c-=k; k*=2; } if(c) // 余下的组 { cnt++; v[cnt]=c*a; w[cnt]=c*b; } } n=cnt; // 分完组,转变成01背包问题 for(int i=1;i<=n;i++) for(int j=m;j>=v[i];j--) f[j]=max(f[j],f[j-v[i]]+w[i]); printf("%d",f[m]); return 0; }
分组背包问题
物品有若干组,每组有若干种,每组最多选一个
看每组物品一个不选或者选谁
#include<iostream> #include<algorithm> using namespace std; const int N=110; int v[N][N],w[N][N],s[N]; int f[N]; int n,m; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&s[i]); for(int j=1;j<=s[i];j++) { scanf("%d%d",&v[i][j],&w[i][j]); } } // 和01背包类似多一步找每组的最优解 for(int i=1;i<=n;i++) for(int j=m;j>=0;j--) for(int k=1;k<=s[i];k++) if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]); printf("%d",f[m]); return 0; }
线性DP
数字三角形
#include<iostream> #include<algorithm> using namespace std; const int N=510,INF=1e9; int a[N][N]; int f[N][N]; int 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]); for(int i=0;i<=n;i++) for(int j=0;j<=i+1;j++) f[i][j]=-INF; f[1][1]=a[1][1]; // 用集合存当前到达该点的所有路径的最优解 for(int i=2;i<=n;i++) 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]); printf("%d",res); return 0; }
最长上升子序列
找以每个数为底的所有情况,取最大值
状态计算是在i前面所有小于a[i]的f[]的最大值
#include<iostream> #include<algorithm> using namespace std; const int N=1010; int a[N],f[N]; int 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]) { f[i]=max(f[i],f[j]+1); } } } int res=0; for(int i=1;i<=n;i++) res=max(res,f[i]); printf("%d",res); return 0; }
最长公共子序列
有两个字符串,集合是存在第一个序列前i个字母中出现,且在第二个序列的前i个字母中出现的子序列
a表示第一个序列,b表示第二个序列
状态计算是将其分成四种情况,a[i],b[j],都不要,第一个不要,第二个要,第一个要,第二个不要,都要。
00,11的状态好表示,但是01,10,不好表示,可以用f[i-1,j],f[i,j-1]表示,这两个包含01,10情况,但是f[i,j] 包含这两个,所以可以用这两个表示01,10。而且00包含于f[i-1,j],f[i,j-1],所以一般不写了。
#include<iostream> #include<algorithm> using namespace std; const int N=1010; char a[N],b[N]; int f[N][N]; int n,m; int main() { scanf("%d%d",&n,&m); scanf("%s%s",a+1,b+1); // 从1的位置读取数组 // 精髓,简洁的代码,复杂的思想,最好的理解是手推一下 for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { // 计算情况分为四种,a[i],b[j],不相等且和先前的字母也不相同即00, // a[i]和b[j-1]相同即为10,同理另一个是01,10。 // 01,10,包含了00的情况,所以不用特判。 // 这三种情况都是求他们先前的最大值。两个数不相等和相等是两个无关联的情况。 // 所以先判这个不影响,如果相同再特判 f[i][j]=max(f[i-1][j],f[i][j-1]); // 这一步才是计数的,当他俩相同时,计数加上1,而且同一层j往后都会保留这个数 // 并且在同一层循环中,数不会在变 // 因为如果再遇见相同的数,都是f[i-1][j-1]+1,数值不变 if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1); } printf("%d",f[n][m]); return 0; }
区间Dp
合并石子
状态计算是最后一次合并左边有1个,2个,。。。。k-1个k是右边界
#include<iostream> #include<algorithm> using namespace std; const int N= 310; int a[N],s[N]; int f[N][N]; int n; int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); s[i]=s[i-1]+a[i]; } // 集合的含义是 从i 到 j 中最小的合并值。 for(int len=2;len<=n;len++) // 是合并的长度,合并的长度依次增加才能算。 for(int i=1;i+len-1<=n;i++) // 定起始点 { int l=i,r=i+len-1; // 确定区间的始末点 f[l][r]=1e8; // 因为是取最小值,所以先定一个最大值 // 状态计算是用合并间隔点来区分,即最后一次在哪里合并 for(int k=l;k<r;k++) // 从开头到结尾 f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]); // 合并k两边的所用最小的再加上一共的(用前缀和记录) } printf("%d",f[1][n]); // 从1到n的最小值就是答案 return 0; }