基本算法
离散化
sort(v.begin(),v.end()); //排序
v.erase(unique(v.begin(),v.end()),v.end()); //去重
倍增RMQ
寻找区间最大值算法
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+5;
int dp[N][20];
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m;
cin>>n;
for(int i=1;i<=n;i++)cin>>dp[i][0];
for(int i=1;(1<<i)<=n;i++){
for(int j=1;j+(1<<i)-1<=n;j++){
dp[j][i]=max(dp[j][i-1],dp[j+(1<<(i-1))][i-1]);
}
}
cin>>m;
while(m--){
int l,r,k=0;
cin>>l>>r;
while((1<<(k+1))<=r-l+1)k++;
cout<<max(dp[l][k],dp[r-(1<<k)+1][k])<<'\n';
}
return 0;
}
**题目:**乐乐在游览一个浏览值为 u 的星球 v 后,他将游览的下一个星球是编号大于 v、浏览值大于 u 的,编号最小的星球。乐乐将一共访问 yi 个星球,请你输出 他最后一个访问的星球编号,如果他不能访问 yi 个星球,输出-1
寻找第k个父节点,借助单调队列
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int a[N],fa[N][20];
stack<int>q;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int t;
cin>>t;
while(t--){
int n,m;
cin>>n>>m;
memset(fa,0,sizeof fa);
while(!q.empty())q.pop();
for(int i=1;i<=n;i++){
cin>>a[i];
while(!q.empty()&&a[q.top()]<a[i]){
auto u=q.top();
q.pop();
fa[u][0]=i;
}
q.push(i);
}
for(int i=1;i<20;i++){
for(int u=1;u<=n;u++){
fa[u][i]=fa[fa[u][i-1]][i-1];
}
}
while(m--){
int u,k,sum=0;
cin>>u>>k;
k--;
for(int i=19;i>=0;i--){
if(sum+(1<<i)<=k){
u=fa[u][i];
sum+=(1<<i);
}
}
if(!u)u=-1;
cout<<u<<'\n';
}
}
return 0;
}
归并排序
题目:逆序对的数量
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。
假设区间(l,mid)和(mid+1,r)中存在p1<p2而a[p1]>a[p2],则区间a[p1]到a[mid]的数肯定都大于a[p2],所以逆序对应该为mid-p1+1既p1到mid内的数的个数。
#include<bits/stdc++.h>
using namespace std;
const int mxn=5e5+5;
int a[mxn],b[mxn],n;
long long ans;
void merge(int l,int mid,int r){ //mid之前一定是升序
int p=l,p1=l,p2=mid+1;
while(p1<=mid&&p2<=r){
if(a[p1]<=a[p2])b[p++]=a[p1++];
else b[p++]=a[p2++],ans+=mid-p1+1;
}
while(p1<=mid)b[p++]=a[p1++];
while(p2<=r)b[p++]=a[p2++];
for(int i=l;i<=r;i++)a[i]=b[i];
}
void merge(int l,int r){
if(l==r)return;
int mid=l+r>>1;
merge(l,mid);
merge(mid+1,r);
merge(l,mid,r);
}
int main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
merge(1,n);
cout<<ans;
return 0;
}
多路归并
多路排并也是排序,将几个不同的有序序列合并成一个有序序列,例如a[1]=[1,3,5],a[2]=[1,4,7],a[3]=[2,2,3],则多路排并后应该=[1,1,2,2,3,3,4,5,7]
其实就是用优先队列,将队列中的最小或最大取出
该题目是将几个等差数列取出其中最大time个数
for(int i=1;i<=n;i++)q.push({a[i],i});
int res=0;
while(!q.empty()){
auto u=q.top();
q.pop();
int numb=u.first;
int id=u.second;
if(time<=0)break;
time--;
res+=numb;
if(numb-d[id]>0)q.push({numb-d[id],id});
}
return res;
数据结构
树链剖分
dfs1把所有节点的父节点、深度、树的大小以及重链求出来
void dfs1(int u,int father){
fa[u]=father,dep[u]=dep[father]+1,sz[u]=1;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(j==father)continue;
dfs1(j,u);
sz[u]+=sz[j];
if(sz[son[u]]<sz[j])son[u]=j;
}
}
dfs2求出每个节点所在链的根节点,以 及给每个节点赋予一个新值
先遍历重链,可以保证一条链上的节点的编号相连
void dfs2(int u,int t){
id[u]=++cnt;
top[u]=t;
ne[cnt]=a[u];
if(!son[u])return;
dfs2(son[u],t);
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(j==fa[u]||j==son[u])continue;
dfs2(j,j);
}
}
询问树上一条链上的总和或者最大值
如果u、v头节点不同,说明在两条不同的链上
每次应求深度深的那条链上
int query_path(int u,int v,int d){
int res=(d==0?-1000000000:0);
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])swap(u,v);
if(d)res+=query(1,1,n,id[top[u]],id[u],d);
else res=max(res,query(1,1,n,id[top[u]],id[u],d));
u=fa[top[u]];
}
if(dep[u]<dep[v])swap(u,v);
if(d)res+=query(1,1,n,id[v],id[u],d);
else res=max(res,query(1,1,n,id[v],id[u],d));
return res;
}
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=3e4+5,M=N<<1;
struct node{
int sum,w;
}tr[N<<2];
int head[N],to[M],from[M],idx;
int dep[N],sz[N],id[N],cnt;
int top[N],a[N],fa[N],son[N];
int n,q,ne[N];
void add(int u,int v){
to[idx]=v,from[idx]=head[u],head[u]=idx++;
}
void pushup(int u){
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
tr[u].w=max(tr[u<<1].w,tr[u<<1|1].w);
}
void dfs1(int u,int father){
fa[u]=father,dep[u]=dep[father]+1,sz[u]=1;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(j==father)continue;
dfs1(j,u);
sz[u]+=sz[j];
if(sz[son[u]]<sz[j])son[u]=j;
}
}
void dfs2(int u,int t){
id[u]=++cnt,top[u]=t,ne[cnt]=a[u];
if(!son[u])return;
dfs2(son[u],t);
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(j==fa[u]||j==son[u])continue;
dfs2(j,j);
}
}
void build(int u,int l,int r){
if(l==r)tr[u]={ne[l],ne[l]};
else{
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
}
int query(int u,int l,int r,int L,int R,int d){
if(l>=L&&r<=R){
if(d)return tr[u].sum;
else return tr[u].w;
}else{
int mid=l+r>>1,v=(d==0?-1000000000:0);
if(L<=mid){
if(d)v+=query(u<<1,l,mid,L,R,d);
else v=max(v,query(u<<1,l,mid,L,R,d));
}
if(R>mid){
if(d)v+=query(u<<1|1,mid+1,r,L,R,d);
else v=max(v,query(u<<1|1,mid+1,r,L,R,d));
}
return v;
}
}
int query_path(int u,int v,int d){
int res=(d==0?-1000000000:0);
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])swap(u,v);
if(d)res+=query(1,1,n,id[top[u]],id[u],d);
else res=max(res,query(1,1,n,id[top[u]],id[u],d));
u=fa[top[u]];
}
if(dep[u]<dep[v])swap(u,v);
if(d)res+=query(1,1,n,id[v],id[u],d);
else res=max(res,query(1,1,n,id[v],id[u],d));
return res;
}
void modify(int u,int l,int r,int x,int y){
if(l==r)tr[u]={y,y};
else{
int mid=l+r>>1;
if(x<=mid)modify(u<<1,l,mid,x,y);
else modify(u<<1|1,mid+1,r,x,y);
pushup(u);
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
while(cin>>n>>q){
memset(head,-1,sizeof head);
memset(fa,0,sizeof fa);
memset(dep,0,sizeof dep);
memset(sz,0,sizeof sz);
memset(top,0,sizeof top);
memset(son,0,sizeof son);
cnt=idx=0;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
dfs1(1,0);
dfs2(1,1);
build(1,1,n);
while(q--){
int c,x,y;
cin>>c>>x>>y;
if(c==2)modify(1,1,n,id[x],y);
else cout<<query_path(x,y,c)<<'\n';
}
}
return 0;
}
树状数组
**注意:**x要保证不为0,否则x+=lowbit(x)依旧为0
求逆序对的数量:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int c[N],a[N];
int lowbit(int x){
return x&(-x);
}
void update(int x){
while(x<N){
c[x]++;
x+=lowbit(x);
}
}
int sum(int x){
int res=0;
while(x){
res+=c[x];
x-=lowbit(x);
}
return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n;
cin>>n;
vector<int>v;
for(int i=1;i<=n;i++)cin>>a[i],v.push_back(a[i]);
sort(v.begin(),v.end());
v.erase(unique(v.begin(),v.end()),v.end());
int ans=0;
for(int i=n;i>0;i--){
int x=lower_bound(v.begin(),v.end(),a[i])-v.begin()+1;
ans+=sum(x-1);
update(x);
}
cout<<ans;
return 0;
}
线段树
小度定义一个 01 串的权值为:将 01 串改成相邻字符都不相同所需最小修改的字符个数。
小度初始时有一个 01 串,他有以下两种操作:
1 l r :将区间 [l,r] 中的所有的 0 都变成 1 ,所有的 1 都变成 0 。
2 l r :查询区间 [l,r] 子串的权值。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=3e5+5;
int a[N];
struct node{
int l,r;
int w1,w2;
int cnt;
}tr[N<<2];
void pushup(int u){
tr[u].w1=tr[u<<1].w1+tr[u<<1|1].w1;
tr[u].w2=tr[u<<1].w2+tr[u<<1|1].w2;
}
void build(int u,int l,int r){
tr[u]={l,r,0,0,0};
if(l==r){
if(l%2){
if(a[l]==1)tr[u].w2++;
else tr[u].w1++;
}else{
if(a[l]==1)tr[u].w1++;
else tr[u].w2++;
}
return;
}
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
void pushdown(int u){
tr[u].cnt=0;
swap(tr[u<<1].w1,tr[u<<1].w2);
tr[u<<1].cnt=(tr[u<<1].cnt+1)%2;
swap(tr[u<<1|1].w1,tr[u<<1|1].w2);
tr[u<<1|1].cnt=(tr[u<<1|1].cnt+1)%2;
}
void update(int u,int L,int R){
if(tr[u].l>=L&&tr[u].r<=R){
swap(tr[u].w1,tr[u].w2);
tr[u].cnt=(tr[u].cnt+1)%2;
return;
}
if(tr[u].cnt)pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(L<=mid)update(u<<1,L,R);
if(R>mid)update(u<<1|1,L,R);
pushup(u);
}
node query(int u,int L,int R){
if(tr[u].l>=L&&tr[u].r<=R)return tr[u];
if(tr[u].cnt)pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
node trL={0,0,0,0,0},trR={0,0,0,0,0};
if(L<=mid)trL=query(u<<1,L,R);
if(R>mid)trR=query(u<<1|1,L,R);
return {tr[u].l,tr[u].r,trL.w1+trR.w1,trL.w2+trR.w2,0};
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,q;
cin>>n>>q;
string s;
cin>>s;
for(int i=0;i<s.size();i++){
if(s[i]=='1')a[i+1]=1;
}
build(1,1,n);
while(q--){
int c,l,r;
cin>>c>>l>>r;
if(c==1)update(1,l,r);
else{
auto now=query(1,l,r);
cout<<min(now.w1,now.w2)<<'\n';
}
}
return 0;
}
扫描线
**题目:**矩形的表示格式为 (x1,y1,x2,y2),代表矩形的两个对角点坐标。为了醒目,总部要求对所有机器人选中的矩形区域涂黄色油漆。小明并不需要当油漆工,只是他需要计算一下,一共要耗费多少油漆。其实这也不难,只要算出所有矩形覆盖的区域一共有多大面积就可以了。
1≤n≤10000,
0≤x1,x2,y2,y2≤10000
**思路:**把这两个矩形分成三个,把每次给的矩形的纵坐标分成增线和删线两部分,再把x坐标从小到大排序,每次ans=新加入的x坐标与上一次加入的x坐标差值*上一次结束以后y坐标在区间内的总长度。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e4+5;
struct node{
int x,y1,y2,k;
bool operator<(const node&b)const{
return x<b.x;
}
}nb[N*2];
struct Tree{ //1.节点应该存它的左端点、右端点,cnt记录的是y的值在l-r范围内被加或减了几次,如果不为0,说明前面覆盖过这个区间,len记录l-r范围内,不一定是连续的长度
int l,r,cnt,len;
}tr[N*4];
void build(int u,int l,int r){
tr[u]={l,r,0,0};
if(l==r)return;
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
}
void pushup(int u){ //2.懒标记,向上更新区间内如果cnt不为0,说明该区间被覆盖,长度应该等于r-l+1否则不被覆盖(被删完),长度=左子树的长度+右子树的长度(不需要连续)
if(tr[u].cnt)tr[u].len=tr[u].r-tr[u].l+1;
else if(tr[u].l==tr[u].r)tr[u].len=0;
else tr[u].len=tr[u<<1].len+tr[u<<1|1].len;
}
void modify(int u,int l,int r,int x){ //3.每次修改l-r区间内的值,如果x是1说明是增加一次覆盖,-1说明是减少一次覆盖如果节点的l到r被包围,说明都被覆盖
if(tr[u].l>=l&&tr[u].r<=r){
tr[u].cnt+=x;
pushup(u);
}else{
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid)modify(u<<1,l,r,x);
if(r>mid)modify(u<<1|1,l,r,x);
pushup(u);
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,tot=0;
cin>>n;
for(int i=1;i<=n;i++){
int xx1,yy1,xx2,yy2;
cin>>xx1>>yy1>>xx2>>yy2;
nb[++tot]={xx1,yy1,yy2,1};
nb[++tot]={xx2,yy1,yy2,-1};
}
sort(nb+1,nb+1+tot);
build(1,0,10000);
int ans=0;
for(int i=1;i<=tot;i++){
if(i>1)ans+=(nb[i].x-nb[i-1].x)*(tr[1].len);
modify(1,nb[i].y1,nb[i].y2-1,nb[i].k);
}
cout<<ans;
return 0;
}
可持久化线段树
主要用于找区间第k小和第k大的数
第一是离散化并建一颗空树,树记录的是某个下标的存入数量
插入操作,用root数组记录每个新插入的点的根节点指针,如果当前的x在[l,mid]区间内,则改变左子树的值,右子树与上一颗树相同,反之改变右子树的值
如果在区间[l,r]内,如果k的值大于[l,mid]的值,说明第k小的数在[mid+1,r]区间内,此时只要找[mid+1,r]区间内第k-[l,mid].cnt,反之在[l,mid]区间内则k不变
完整代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int root[N],a[N],idx;
struct node{
int l,r,cnt;
}tr[N*4+N*17];
int build(int l,int r){
int p=++idx;
if(l==r)return p;
int mid=l+r>>1;
tr[p].l=build(l,mid);
tr[p].r=build(mid+1,r);
return p;
}
vector<int>v;
int find(int x){
return lower_bound(v.begin(),v.end(),x)-v.begin();
}
int insert(int pre,int l,int r,int x){
int p=++idx;
tr[p]=tr[pre];
if(l==r){
tr[p].cnt++;
return p;
}
int mid=l+r>>1;
if(x<=mid)tr[p].l=insert(tr[pre].l,l,mid,x);
else tr[p].r=insert(tr[pre].r,mid+1,r,x);
tr[p].cnt=tr[tr[p].l].cnt+tr[tr[p].r].cnt;
return p;
}
int query(int L,int R,int l,int r,int k){
if(l==r)return l;
int cnt=tr[tr[R].l].cnt-tr[tr[L].l].cnt;
int mid=l+r>>1;
if(k<=cnt)return query(tr[L].l,tr[R].l,l,mid,k);
else return query(tr[L].r,tr[R].r,mid+1,r,k-cnt);
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
v.push_back(a[i]);
}
sort(v.begin(),v.end());
v.erase(unique(v.begin(),v.end()),v.end());
root[0]=build(0,v.size()-1);
for(int i=1;i<=n;i++){
root[i]=insert(root[i-1],0,v.size()-1,find(a[i]));
}
while(m--){
int l,r,k;
cin>>l>>r>>k;
cout<<v[query(root[l-1],root[r],0,v.size()-1,k)]<<'\n';
}
return 0;
}
图论
拓扑排序
H-拼高达_第十四届南京工程学院程序设计及应用竞赛校外同步赛 (nowcoder.com)
**概念:**设有a,b,c,d等事情,其中a有最高优先级,b和c优先级相同,d最低,那么abcd或acbd都是可行的排序
通俗来讲就是有优先级的排序
可以设一个b数组来存储节点v要比u的优先级低
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
b[v]++;
}
把其中优先级最高的放入队列中
for(int i=1;i<=n;i++){
if(!b[i])q.push(i);
}
将优先级高的先取出,如果某个点的所有比他优先级高的都取出了,那这个点也就能存入队列中
while(!q.empty()){
auto u=q.front();
q.pop();
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(--b[j]==0)q.push(j);
}
}
最短路径
void dijkstr(){
priority_queue<PII,vector<PII>,greater<PII>>q;
memset(dist,-1,sizeof dist);
dist[T]=0;
q.push({0,T});
while(!q.empty()){
auto now=q.top();
q.pop();
int u=now.second;
int d=now.first;
for(int i=rhead[u];i!=-1;i=rfrom[i]){
int j=rto[i];
if(dist[j]!=-1&&dist[j]<=d+rval[i])continue;
dist[j]=d+ravl[i];
q.push({dist[j],j});
}
}
}
双向A*算法
**思路:**只要先求得从终点到各点的距离后,再从起点开始用优先队列遍历,建立一个数组cnt记录到达某点的次数,如果到达某点的次数==K,那就不可能还经历了,因为第K短路径上的每个点走过的次数一定不超过K
初始化的时候可以用head记录从起点到别的点的有向边,用rhead记录从终点到别的点的有向边,其他数组可以共享使用
求从终点到达各点的最短路径
int dist[N];
int S,T,K;
void dijkstr(){
priority_queue<PII,vector<PII>,greater<PII>>q;
memset(dist,-1,sizeof dist);
dist[T]=0;
q.push({0,T});
while(!q.empty()){
auto now=q.top();
q.pop();
int u=now.second;
int d=now.first;
for(int i=rhead[u];i!=-1;i=from[i]){
int j=to[i];
if(dist[j]!=-1&&dist[j]<=d+val[i])continue;
dist[j]=d+val[i];
q.push({dist[j],j});
}
}
}
用优先队列存从起点到达某点的路径+该点到达终点的最短路径,依次从小到大进行排序,如果遍历终点K次,则第K次遍历到终点即是第K短路径
int cnt[N];
int solve(){
priority_queue<PIII,vector<PIII>,greater<PIII>>q;
q.push({dist[S],{0,S}});
while(!q.empty()){
auto now=q.top();
q.pop();
int d=now.second.first;
int u=now.second.second;
cnt[u]++;
if(cnt[T]==K)return d;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(cnt[j]>K)continue;
q.push({dist[j]+val[i]+d,{d+val[i],j}});
}
}
return -1;
}
欧拉路径
**概念:**从图中某个点出发,遍历整个图,图中每条边通过且通过一次。
欧拉回路是起点和终点相同的欧拉路
**判断:**无向连通图如果图中的点全都是偶点,则存在欧拉回路,任意点都可以作为起点和终点。如果只有两个奇点,则存在欧拉路。
有向连通图的话判断:
1.除了起点和终点外其余点入读=出度(即如果有起点则一定有终点,起点和终点的入读和出度相差1)
2.是否连通(用并查集判断是否所有都连通)
这里用din表示入度,dout表示出度
for(int i=0;i<26;i++)fa[i]=i;
for(int i=1;i<=n;i++){
string s;
cin>>s;
int l=s[0]-'a',r=s[s.size()-1]-'a';
vis[l]=vis[r]=true;
din[l]++,dout[r]++;
fa[find(l)]=find(r);
}
如果入度!=出度,判断它有没有可能是起点或终点,都不是则无法成立欧拉路径,最后还要判断起点和终点最多只能有1个
for(int i=0;i<26;i++){
if(din[i]!=dout[i]){
if(din[i]==dout[i]+1)st++;
else if(din[i]+1==dout[i])ed++;
else{
flag=true;
break;
}
}
}
通过并查集来判断连通性
int res=-1;
for(int i=0;i<26;i++){
if(!vis[i])continue;
if(res==-1)res=find(i);
else if(res!=find(i))flag=true;
}
最近公共祖先
1.采用倍增的思路,在从根节点向下遍历的时候,给所有节点往上的父节点标记
void dfs(int u,int father){
if(father!=-1){
dep[u]=dep[father]+1;
fa[u][0]=father;
}
for(int i=1;i<=19;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(j==father)continue;
dfs(j,u);
}
}
2.找两个节点的最近公共祖先,先把深度更深的节点向上遍历,直到与另一个节点的深度达到一致
如果深度一致以后两个节点不相同,说明在根节点的两测,那么再向上便利直到两个节点相同
int lca(int u,int v){
if(dep[u]<dep[v])swap(u,v);
for(int i=19;i>=0;i--){
if(dep[fa[u][i]]>=dep[v])
u=fa[u][i];
}
if(u==v)return v;
for(int i=19;i>=0;i--){
if(fa[u][i]!=fa[v][i])
u=fa[u][i],v=fa[v][i];
}
return fa[u][0];
}
带权值
**题目:**A 国有 n 座城市,编号从 1 到 n ,城市之间有 m 条双向道路。每一条道路对车辆都有重量限制,简称限重。现在有 q 辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。
**思路:**1.最后肯定不会有圈,所以是树,但不一定是一棵树,可能是好几颗树,用生成最小生成树的方法生成一颗每条边是最大的连通图
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++)cin>>nb[i].u>>nb[i].v>>nb[i].w;
sort(nb+1,nb+1+m);
memset(head,-1,sizeof head);
for(int i=1;i<=m;i++){
int u=find(nb[i].u),v=find(nb[i].v);
if(u!=v){
fa[u]=v;
add(nb[i].u,nb[i].v,nb[i].w);
add(nb[i].v,nb[i].u,nb[i].w);
}
}
2.增设一个倍增的路径上的权值,要求从叶子节点到根节点的值一定是小于这条线上最小的限重, g [ u ] [ i ] g[u][i] g[u][i], g [ u ] [ 0 ] g[u][0] g[u][0]表示到父节点的限重,那么倍增上去, g [ u ] [ i ] g[u][i] g[u][i]=前面一半的最小限重和后面一半的最小限重得最小值即 m i n ( g [ u ] [ i − 1 ] , g [ d [ u ] [ i − 1 ] ] [ i − 1 ] ) min(g[u][i-1],g[d[u][i-1]][i-1]) min(g[u][i−1],g[d[u][i−1]][i−1])
void dfs(int u,int father,int press){
vis[u]=true;
if(father!=-1){
dep[u]=dep[father]+1;
d[u][0]=father;
g[u][0]=press;
}
for(int i=1;i<=19;i++){
d[u][i]=d[d[u][i-1]][i-1];
g[u][i]=min(g[u][i-1],g[d[u][i-1]][i-1]);
}
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(j==father)continue;
dfs(j,u,val[i]);
}
}
3.找两个点之间得最小限重即找两个节点到最近公共父节点的最小限重中去最小,注意最后的时候要与两个子节点到最近公共父节点的限重进行比较,因为他们下一步相同,所以循环中不会比较这种情况
int lca(int u,int v){
int ans=1000000;
if(dep[u]<dep[v])swap(u,v);
for(int i=19;i>=0;i--){
if(dep[d[u][i]]>=dep[v]){
ans=min(ans,g[u][i]);
u=d[u][i];
}
}
if(u==v)return ans;
for(int i=19;i>=0;i--){
if(d[u][i]!=d[v][i]){
ans=min(ans,min(g[v][i],g[u][i]));
u=d[u][i];
v=d[v][i];
}
}
ans=min(ans,min(g[u][0],g[v][0]));
return ans;
}
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e4+5,M=5e4+5;
struct node{
int u,v,w;
bool operator<(const node&b)const{
return w>b.w;
}
}nb[M];
int fa[N];
int find(int x){
if(x==fa[x])return x;
else fa[x]=find(fa[x]);
return fa[x];
}
int to[M<<1],val[M<<1],from[M<<1],head[N],idx;
void add(int u,int v,int w){
to[idx]=v,val[idx]=w,from[idx]=head[u],head[u]=idx++;
}
int d[N][20],g[N][20];
int dep[N];
bool vis[N];
void dfs(int u,int father,int press){
vis[u]=true;
if(father!=-1){
dep[u]=dep[father]+1;
d[u][0]=father;
g[u][0]=press;
}
for(int i=1;i<=19;i++){
d[u][i]=d[d[u][i-1]][i-1];
g[u][i]=min(g[u][i-1],g[d[u][i-1]][i-1]);
}
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(j==father)continue;
dfs(j,u,val[i]);
}
}
int lca(int u,int v){
int ans=1000000;
if(dep[u]<dep[v])swap(u,v);
for(int i=19;i>=0;i--){
if(dep[d[u][i]]>=dep[v]){
ans=min(ans,g[u][i]);
u=d[u][i];
}
}
if(u==v)return ans;
for(int i=19;i>=0;i--){
if(d[u][i]!=d[v][i]){
ans=min(ans,min(g[v][i],g[u][i]));
u=d[u][i];
v=d[v][i];
}
}
ans=min(ans,min(g[u][0],g[v][0]));
return ans;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++)cin>>nb[i].u>>nb[i].v>>nb[i].w;
sort(nb+1,nb+1+m);
memset(head,-1,sizeof head);
for(int i=1;i<=m;i++){
int u=find(nb[i].u),v=find(nb[i].v);
if(u!=v){
fa[u]=v;
add(nb[i].u,nb[i].v,nb[i].w);
add(nb[i].v,nb[i].u,nb[i].w);
}
}
memset(g,(int)1000000,sizeof g);
for(int i=1;i<=n;i++)
if(!vis[i])dfs(i,-1,(int)1000000);
int q;
cin>>q;
while(q--){
int u,v;
cin>>u>>v;
if(find(u)!=find(v))cout<<-1<<'\n';
else cout<<lca(u,v)<<'\n';
}
return 0;
}
树上差分
如果要在树上的一条边上加值,就是两个节点+1,两个节点的最近公共父节点-1
基环树
一个图包含n个点n条边,特征图中只存在一个环,这样的图被称为基环树(又称换套树)。
F-小红的基环树删边_牛客周赛 Round 44 (nowcoder.com)。
如果是基环树的话,dfs不会超时,因为除了交叉的点即环的出口处会走两条路,其他地方都只走一遍
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int to[N<<1],from[N<<1],val[N<<1],head[N],idx;
int ans[N],n;
void add(int u,int v,int w){
to[idx]=v,from[idx]=head[u],val[idx]=w,head[u]=idx++;
}
bool vis[N];
void dfs(int u,int d){
if(u==n){
for(int i=1;i<=n;i++){
if(!vis[i])ans[i]=min(ans[i],d);
}
return;
}
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i],w=val[i];
if(vis[w])continue;
vis[w]=true;
dfs(j,d+1);
vis[w]=false;
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
memset(head,-1,sizeof head);
memset(ans,1,sizeof ans);
for(int i=1;i<=n;i++){
int u,v;
cin>>u>>v;
add(u,v,i);
add(v,u,i);
}
dfs(1,0);
for(int i=1;i<=n;i++){
if(ans[i]>1e6)cout<<-1<<'\n';
else cout<<ans[i]<<'\n';
}
return 0;
}
Tarjan算法
割边
L-删边游戏_第十一届"图灵杯"NEUQ-ACM程序设计竞赛 (nowcoder.com)
简称找割边算法。
割边:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。
void tarjan(int u,int fa){ //u节点、in_edge上一条边的编号
dfn[u]=low[u]=++num;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(!dfn[j]){ //若j尚未被访问
tarjan(j,u);
low[u]=min(low[u],low[j]);
if(low[j]>dfn[u])v.push_back({j,u});
}else if(j!=fa){ //不是反边
low[u]=min(low[u],dfn[j]);
}
}
}
原理是dfn[x]记录任取一个节点为根节点之后,按树的形状走相当于bfs到x点需要的时间
low[x]记录的是从x点出发所能访问到的最早dfn的值,因此这里dfn是不能改变,low是一直改变的
for(int i=1;i<=n;i++){
if(!dfn[i])tarjan(i,0); //防止不是连通图的情况
}
SCC缩点
void tarjan(int u){
dfn[u]=low[u]=++total;
vis[u]=true,stk[++tot]=u;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(!dfn[j]){ //如果该节点还没有遍历
tarjan(j);
low[u]=min(low[u],low[j]);
}else if(vis[j]) //如果该节点没有被存入到哪个连通块中
low[u]=min(low[u],dfn[j]);
}
if(low[u]==dfn[u]){ //如果该节点的最小遍历值是它自己
++cnt;
int y;
do{
y=stk[tot--];
vis[y]=false;
id[y]=cnt;
numb[cnt]++;
}while(y!=u);
}
}
割点
找割点跟找割边类似,不同之处在于反边也可以用到
先把无向图通过dfs弄成一棵树
1.如果子节点的low值要大于等于该节点的dfn值,说明子节点只能通过走该节点才能得到小的dfn值,则该节点是割点(该节点不是根节点情况下)
2.如果该节点是根节点,那就要判断它子节点数是否大于1||它如果不是第一个进来的节点那它就是割点
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5,M=5e5+5;
int to[M<<1],from[M<<1],head[N],idx;
int cnt[N],dfn[N],low[N],num;
int n,m,ans[N];
bool vis[N];
void add(int u,int v){
to[idx]=v,from[idx]=head[u],head[u]=idx++;
}
void tarjan(int u){
cnt[u]=1,dfn[u]=low[u]=++num;
int flag=0,sum=0;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
cnt[u]+=cnt[j];
if(low[j]>=dfn[u]){
++flag;
ans[u]+=cnt[j]*(n-cnt[j]);
sum+=cnt[j];
if(u!=1||flag>1){
vis[u]=true;
}
}
}else low[u]=min(low[u],dfn[j]);
}
if(vis[u])ans[u]+=(n-1-sum)*(1+sum)+(n-1);
else ans[u]=2*(n-1);
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
memset(head,-1,sizeof head);
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
tarjan(1);
for(int i=1;i<=n;i++)cout<<ans[i]<<'\n';
return 0;
}
强连通分量
**算法思路:**将每个连通块里的所有节点都用一个id标记,强连通分量的题目主要是看入度din和出度dout的问题
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e4+5,M=5e4+5;
int dfn[N],low[N],total;
int stk[N],tot;
int id[N],cnt,numb[N];
bool vis[N];
int to[M],from[M],head[N],idx;
int dout[N],din[N];
void add(int u,int v){
to[idx]=v,from[idx]=head[u],head[u]=idx++;
}
void tarjan(int u){
dfn[u]=low[u]=++total;
vis[u]=true,stk[++tot]=u;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
}else if(vis[j])
low[u]=min(low[u],dfn[j]);
}
if(low[u]==dfn[u]){
++cnt;
int y;
do{
y=stk[tot--];
vis[y]=false;
id[y]=cnt;
numb[cnt]++;
}while(y!=u);
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m;
cin>>n;
memset(head,-1,sizeof head);
for(int i=1;i<=n;i++){
int x;
while(cin>>x&&x!=0){
add(i,x);
}
}
for(int i=1;i<=n;i++){
if(!dfn[i])tarjan(i);
}
for(int i=1;i<=n;i++){
for(int j=head[i];j!=-1;j=from[j]){
int v=to[j];
int u=id[i];
v=id[v];
if(u!=v)dout[u]++,din[v]++;
}
}
int ans=0,flag=0,ans1=0;
for(int i=1;i<=cnt;i++){
if(!dout[i]){
ans++;
}
if(!din[i])ans1++;
}
cout<<ans1<<'\n';
if(cnt==1)cout<<0;
else cout<<max(ans,ans1);
return 0;
}
二分图
int c[N];
bool dfs(int u,int color,int limit){
c[u]=color;
for(int i=head[u];i!=-1;i=from[i]){
if(val[i]<=limit)continue;
int j=to[i];
if(c[j]){ //如果该点涂过了
if(c[j]==color)return false; //并且该点颜色和当前点颜色一样,则不能构造二分图
}
else if(!dfs(j,3-color,limit))return false; //遍历子节点
}
return true;
}
最大匹配
bool dfs(int u){
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(vis[j])continue;
vis[j]=true;
if(!match[j]||dfs(match[j])){ // 如果没有匹配或者它的对象可以改变
match[j]=u;
return true;
}
}
return false;
}
int a[N];
signed main()
int cnt=0;
for(int i=1;i<=n;i++){
memset(vis,0,sizeof vis);
if(dfs(i))cnt++;
}
return 0;
}
扩展
**题目:**给出 n 个点 m 条边 的无向图,给出 k 个点,这 k 个点上每个点都有一个人,每个人每回合能走到一个相邻的节点(不能停留不走),问:有没有可能在某一个回合,让这些人都集中在一个点?
思路:
1.如果这张图是一张二分图,每个点都只能到另一个集合中去,则只有k个节点都在一个集合的条件下才能成立
2.如果这张图不能是一张二分图,则每个节点总能走到染色不了的位置上去,再回到原二分图中,使得每个节点都到一个集合中去
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5,inf=0x3f3f3f3f;
int to[N<<1],from[N<<1],head[N],idx;
void add(int u,int v){
to[idx]=v,from[idx]=head[u],head[u]=idx++;
}
int color[N],a[3];
bool dfs(int u,int c){
color[u]=c;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(!color[j]){
if(dfs(j,3-c))return true;
}else if(color[j]==c)return true;
}
return false;
}
int main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m,k;
cin>>n>>m>>k;
memset(head,-1,sizeof head);
while(m--){
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
if(dfs(1,1)){
cout<<"YES\n";
return 0;
}
for(int i=1;i<=k;i++){
int x;
cin>>x;
a[color[x]]++;
}
if(a[1]==k||a[2]==k)cout<<"YES\n";
else cout<<"NO\n";
return 0;
}
负环
**定义:**环中所有边权值和为负数的环,称为负环
bool spfa(){
stack<int>q;
q.push(0);
for(int i=1;i<=n;i++)add(0,i,1); //这里又添加了0这个节点
while(!q.empty()){
auto u=q.top();
q.pop();
vis[u]=false;
for(int i=head[u];i!=-1;i=from[i]){
int j=to[i];
if(dist[j]<dist[u]+val[i]){
dist[j]=dist[u]+val[i];
cnt[j]=cnt[u]+1;
if(cnt[j]>n)return true; //如果加上这条边总的边数达到n+1,那么至少应该有n+2个节点,负环
if(!vis[j]){
vis[j]=true;
q.push(j);
}
}
}
}
return false;
}
2-SAT
如果a,b矛盾,但是从a出发可以到达b,则选了a会导致选b,矛盾,因此只能选b
构图方法:
一条边u->v的含义:若u则v
命题“若u则v”的逆否命题为“若非v则非u”
动态规划
动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
状态压缩DP
定义:
所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。
状态之间存在关系条件:
除了0,1表示2种状态外,也可以用00,01,10,11两个二进制来表示4个状态,可以此类推
**题目:**最短路径问题,从起点1出发最后回到起点1的最短路径
**思路:**如果再走一个点就走完所有点,则以(这个最后到的点到1位置的距离+当前走的距离)取最小值即为答案
dp[i][j]; //表示状态i=110001,即走过5、4、1三个点且最后一个点在j的最短路径
memset(dp,30000,sizeof dp);
dp[1][0]=0; //一开始起点的位置应该是0
int ans=-1;
for(int i=0;i<(1<<n);i++){
for(int j=0;j<n;j++){
if(((i>>j)&1))continue;
for(int k=0;k<n;k++){
if(!((i>>k)&1))continue;
dp[i+(1<<j)][j]=min(dp[i+(1<<j)][j],dp[i][k]+w[k][j]);
if(i+(1<<j)==(1<<n)-1){
if(ans==-1)ans=dp[i+(1<<j)][j]+w[0][j];
else ans=min(ans,dp[i+(1<<j)][j]+w[0][j]);
}
}
}
}
D-猫狗站队_第十四届南京工程学院程序设计及应用竞赛校外同步赛 (nowcoder.com)
**题目:**挑选出中华田园犬、边牧、柴犬这3种;他又从喵星人中挑出橘猫。所有的狗都不喜欢自己身边有与自己相同的品种;但是这些猫的包容性比较大,可以与任何猫狗相处融洽。
判断一排是否成立
bool check(int x){
int pre=3;
for(int i=1;i<=m;i++){
int now=x&3; //与11进行&运算
if(pre!=3&&now==pre)return true;
pre=now;
x>>=2;
}
return false;
}
判断上下两排是否可以成立
bool check1(int x,int y){
for(int i=1;i<=m;i++){
int u=x&3,c=y&3;
if(u!=3&&u==c)return true;
x>>=2;
y>>=2;
}
return false;
}
**题目:**炮台只能放在平原上,地图一排有平原有山地,并且炮台的攻击范围是横竖2格内,求不发生互相攻击的情况下最多可以放多少台炮台
**思路:**把每一排的平原和山地用二进制来标识1表示平原0表示山地,1001011100
for(int i=1;i<=n;i++){
string s;
cin>>s;
for(int j=0;j<m;j++){
mp[i]+=(s[j]=='P'?1:0)<<(m-1-j);
}
}
把炮台可以同时放两排的情况用vector存储
bool check(int x){
return (x&(x>>2)||(x&(x>>1)));
}
for(int i=0;i<v.size();i++){
for(int j=0;j<v.size();j++){
if(v[i]&v[j])continue;
g[v[i]].push_back(v[j]);
}
}
动态规划最后两种排序=前两种
for(int i=1;i<=n;i++){
for(int j=0;j<v.size();j++){
int a=v[j];
if((a|mp[i])^mp[i])continue;
for(int k=0;k<g[a].size();k++){
int b=g[a][k];
for(int p=0;p<g[b].size();p++){
int c=g[b][p];
if(a&c)continue;
dp[i&1][a][b]=max(dp[i&1][a][b],dp[(i-1)&1][b][c]+nb[a]);
ans=max(ans,dp[i&1][a][b]);
}
}
}
}
结合背包问题
**题目:**遗憾的是老板并不单独出售糖果,而是 K 颗一包整包出售。
幸好糖果包装上注明了其中 K 颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。
给定 N 包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。
1≤N≤100,
1≤M,K≤20,
**思路:**题目给的M范围很小,所以可以考虑用状态来表示糖果口味
就可以考虑用状态dp,得出每种状态最少需要买几包
dp[j]=min(dp[j],dp[j&(~x)]+1);
#include<bits/stdc++.h>
using namespace std;
#define int long long
int a[110][30];
int dp[1<<21];
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m,k;
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=k;j++){
cin>>a[i][j];
}
}
for(int i=1;i<(1<<m);i++)dp[i]=n+1;
for(int i=1;i<=n;i++){
int x=0;
for(int j=1;j<=k;j++){
x|=(1<<(a[i][j]-1));
}
for(int j=0;j<(1<<m);j++){
dp[j]=min(dp[j],dp[j&(~x)]+1);
}
}
if(dp[(1<<m)-1]==n+1)cout<<"-1\n";
else cout<<dp[(1<<m)-1];
return 0;
}
哈密顿路
Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。
**题目:**给定一张 n 个点的带权无向图,点从 0∼n 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。
1≤n≤20
**思路:**n的范围是1到20,用dfs的话20!会超时,但2的20次方小于1e8(27次以下可以),这题就可以用二进制来写
如果i二进制101111001,其中1表示走过,那 d p [ i ] [ j ] dp[i][j] dp[i][j]这个值就是走过9,7,6,5,4,1这些位置且终点是j的值,那么i-1二进制即101111000就表示走过9,7,6,5,4这几个位置且的值,那么 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + a [ 1 ] [ j ] dp[i][j]=dp[i-1][j]+a[1][j] dp[i][j]=dp[i−1][j]+a[1][j],以此就可以写递推方程式。
memset(dp,inf,sizeof dp);
dp[0][0]=0; //起点为0
for(int i=1;i<(1<<n);i++){
for(int j=0;j<n;j++){
if((i>>j)&1){
for(int k=0;k<n;k++){
if((i>>k)&1)
dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+a[j][k]);
}
}
}
}
cout<<dp[(1<<n)-1][n-1]; //终点为n-1
区间DP
区间dp,顾名思义,在区间上dp,大多数题目的状态都是由区间(类似于 d p [ l ] [ r ] dp[l][r] dp[l][r]这种形式)构成的,就是我们可以把大区间转化成小区间来处理,然后对小区间处理后再回溯的求出大区间的值,主要的方法有两种,记忆化搜索和递推。
**题目:**每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
d p [ l ] [ r ] = m i n ( d p [ l ] [ k ] + d p [ k + 1 ] [ r ] + a [ r ] − a [ l − 1 ] ) dp[l][r]=min(dp[l][k]+dp[k+1][r]+a[r]-a[l-1]) dp[l][r]=min(dp[l][k]+dp[k+1][r]+a[r]−a[l−1])
for(int len=1;len<n;len++){
for(int l=1;l<=n-len;l++){
int r=l+len;
for(int k=l;k<r;k++){
dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+a[r]-a[l-1]);
}
}
}
**题目:**给出一个圆,圆上等距分布 n 个点,编号为 1∼n。另有 m 条线段,第 i 条线段的端点为 xi 和 yi,权重为 wi。定义一个圆是优良的,当且仅当所有线段无交(端点重合也算相交)。
**思路:**可以把这个问题转换成求1到2区间的不重合权重、1到3区间···,再到2到3区间不重合权重,以此类推求出1到n区间不重合权重最大值,所有权重和-最大值即为答案
for(int i=1;i<=n;i++){
for(int j=i-1;j>=1;j--){
dp[j][i]=max(a[j][i]+dp[j+1][i-1],dp[j+1][i]);
for(auto x:v[j]){
if(x>=i)continue;
dp[j][i]=max(dp[j][i],dp[j][x]+dp[x+1][i]);
}
res=max(res,dp[j][i]);
}
}
队列优化
**题目:**有 N 块木板从左到右排成一行,有 M 个工匠对这些木板进行粉刷,每块木板至多被粉刷一次。
第 i 个木匠要么不粉刷,要么粉刷包含木板 Si 的,长度不超过 Li 的连续的一段木板,每粉刷一块可以得到 Pi 的报酬。
不同工匠的 Si 不同。
请问如何安排能使工匠们获得的总报酬最多。
/*
思路:
1.状态表示dp[i][j]:
集合:只考虑前i个工匠只刷前j面墙
属性:最大值
2.状态计算:
第i个工匠不刷则=dp[i-1][j]
第i个工匠刷:
第j块木板不刷=dp[i][j-1]
第j块木板刷:找到前面一个k满足dp[i-1][k]+(j-k)*p[i]的值最大
3.找k这个点用双端队列来求
双端队列存区间[j-l,j]内dp[i-1][k]-k*p[i]从大到小排
则dp[i][j]=j*p[i]+(dp[i-1][k]-k*p[i])=dp[i-1][k]+(j-k)*p[i]的值最大
*/
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=16005,M=105;
struct node{
int l,p,s;
bool operator<(const node&b)const{
return s<b.s;
}
}nb[M];
int dp[M][N];
deque<pair<int,int>>q;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++)cin>>nb[i].l>>nb[i].p>>nb[i].s;
sort(nb+1,nb+1+m);
for(int i=1;i<=m;i++){
while(!q.empty())q.pop_front();
for(int j=0;j<=n;j++){
dp[i][j]=dp[i-1][j];
if(j)dp[i][j]=max(dp[i][j],dp[i][j-1]);
if(j>=nb[i].s){
while(q.size()&&q.front().second<j-nb[i].l)q.pop_front();
if(q.size())dp[i][j]=max(dp[i][j],q.front().first+nb[i].p*j);
}else {
while(q.size()&&q.back().first<=dp[i-1][j]-nb[i].p*j)q.pop_back();
q.push_back({dp[i-1][j]-nb[i].p*j,j});
}
}
}
cout<<dp[m][n];
return 0;
}
数学知识
公约数|倍数
最小公约数函数__gcd(x,y);
最大公倍数函数lcm(x,y);
联系:lcm(x,y)=x*y/gcd(x,y);
快速幂
int qmi(int a,int b,int p){
int res=1;
while(b){
if(b&1)res=res*a%p;
b>>=1;
a=a*a%p;
}
return res%p; //考虑到b=0的情况
}
龟速乘
int qmi(int a,int b,int p){
int res=0;
while(b){
if(b&1)res=(res+a)%p;
b>>=1;
a=(a<<1)%p;
}
return res%p;
}
矩阵乘法
用 T(n)=(F1+2F2+3F3+…+nFn)mod m 表示 Fibonacci 数列前 n 项变形后的和 mod m 的值。现在佳佳告诉你了一个 n 和 m,请求出 T(n) 的值。
Tn=(n+1)*Sn-Rn
Rn=S1+S2+…+Sn
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
struct node{
int mp[4][4]={
1,1,0,0,
1,0,0,0,
1,1,1,0,
1,1,1,1
};
node operator*(const node&b)const{
node ans;
memset(ans.mp,0,sizeof ans.mp);
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
for(int k=0;k<4;k++){
ans.mp[i][j]=(ans.mp[i][j]+mp[i][k]*b.mp[k][j]%m)%m;
}
}
}
return ans;
}
};
node qp(node a,int b){
node res;
memset(res.mp,0,sizeof res.mp);
for(int i=0;i<4;i++)res.mp[i][i]=1;
while(b){
if(b&1)res=res*a;
a=a*a;
b>>=1;
}
return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
if(n==1)cout<<1%m;
else if(n==2)cout<<3%m;
else{
node a;
node ans=qp(a,n-2);
int Sn=ans.mp[2][0]+ans.mp[2][1]+ans.mp[2][2]*2;
int Rn=ans.mp[3][0]+ans.mp[3][1]+ans.mp[3][2]*2+ans.mp[3][3]*3;
cout<<(Sn*(n+1)%m-Rn%m+m)%m;
}
return 0;
}
裴蜀定理
若a,b是整数,且gcd(a,b)=d,那么对于任意的整数x,y,ax+by都一定是d的倍数,特别地,一定存在整数x,y,使ax+by=d成立。
对于多项式同理,如果ax+by+cz=d,d必定是__gcd(a,b,c)的倍数
扩展欧几里得
用于求解ax+by=gcd(a,b)的解,当b=0时候,ax+by=a故而x=1,y=0
只要知道gcd(a,b)==gcd(b,a%b)就能求解出来特解x0和y0
则所有的通解可以表示为 x = x 0 + ( b / d ) ∗ k , y = y 0 − ( a / d ) ∗ k x=x0+(b/d)*k,y=y0-(a/d)*k x=x0+(b/d)∗k,y=y0−(a/d)∗k,其中k可以是任意整数
置换法
置换环是用来求解数组排序元素间所需最小交换次数这类问题。
置换环思想:置换环将每个元素指向其排序后应在的位置,最终首位相连形成一个环**(若数字在最终位置,则其自身成环),可知元素之间的交换只会在同一个环内进行,而每个环内的最小交换次数为环上元素数量-1**
我们将所有环的交换次数加起来得到
ans= ∑ i n ∑_i^n ∑in( c y c l e s i z e i cyclesize_i cyclesizei-1),其中cyclesize为环上元素数量。
最后化简公式得,排列所需最小交换次数为数组长度-环的个数。
交换带条件
**题目:**小红拿到了一个排列,其中初始所有元素都是白色,但有一些元素被染成了红色。小红每次操作可以选择交换任意一个红色元素和一个白色元素的位置。她希望操作尽可能少的次数使得数组变成升序,你能帮帮她吗?
**思路:**如果环内有两种颜色,只要环内元素数量-1;如果是单色,则需调用环外一个元素,所以要环内元素数量-1+2即环内元素数量+1;但若有两个环都是纯单色,且一个是R一个是W,则可以少去2次交换(互相抵消);所以最后答案=n-环的个数+max(cntR,cntW)*2。
int sum=0,cntr=0,cntw=0;
for(int i=1;i<=n;i++){
if(vis[i])continue;
else sum++;
int u=i,cnt=0;
int fr=0,fw=0;
while(!vis[u]){
++cnt;
vis[u]=true;
fr|=(s[u]=='R'?1:0);
fw|=(s[u]=='W'?1:0);
u=a[u];
}
if(cnt>1){
if(!fr)cntr++;
if(!fw)cntw++;
}
}
cout<<n-sum+max(cntr,cntw)*2;
同余
若整数a和整数b除以正整数m的余数相等,则称a,b
要求解一元线性同余方程:
1.用扩展欧几里得方程得到特解x0,y0,此时可以得到 a ∗ x 0 + b ∗ y 0 = g c d ( a , b ) a*x0+b*y0=gcd(a,b) a∗x0+b∗y0=gcd(a,b)
2.那么要求解 a ∗ x = b ( m o d m ) ,即求解 a ∗ x + m ∗ y = b a*x=b(mod m),即求解a*x+m*y=b a∗x=b(modm),即求解a∗x+m∗y=b
3.根据裴蜀定理如果b不能被gcd(a,m)整除,方程不成立
4.如果方程成立就可以得到 a ∗ x 0 / g c d ( a , n ) ∗ b + m ∗ y 0 / g c d ( a , m ) ∗ b = b a*x0/gcd(a,n)*b+m*y0/gcd(a,m)*b=b a∗x0/gcd(a,n)∗b+m∗y0/gcd(a,m)∗b=b
5.所以x=x0*b/gcd(a,m)
6.有m/gcd(a,m)个模m的不同余的解,所以最后答案应该对m/gcd(a,m)取模
费马小定理
设 n 是素数, a 是正整数且与 n 互素,有 a n − 1 = 1 ( m o d 设n是素数,a是正整数且与n互素,有a^{n-1}=1(mod 设n是素数,a是正整数且与n互素,有an−1=1(mod n ) 。即 a ∗ a n − 2 = 1 ( m o d n)。即a*a^{n-2}=1(mod n)。即a∗an−2=1(mod n ) ,那么 a n − 2 就是 a 模 n 的逆 n),那么a^{n-2}就是a模n的逆 n),那么an−2就是a模n的逆
欧拉函数
void get_eulers(int n){
int tot=0;
phi[1]=1;
for(int i=2;i<=n;i++){
if(!phi[i]){
phi[i]=i-1;
pre[++tot]=i;
}
for(int j=1;j<=tot;j++){
if(pre[j]*i>n)break;
if(i%pre[j]==0){
phi[i*pre[j]]=phi[i]*pre[j];
break;
}
else phi[i*pre[j]]=phi[i]*(pre[j]-1);
}
}
}
欧拉筛因子数
原理: n = p 1 r 1 ∗ p 2 r 2 ∗ . . . ∗ p m r m n=p_1^{r1}*p_2^{r2}*...*p_m^{rm} n=p1r1∗p2r2∗...∗pmrm, 则 n 的因子个数为: ( p 1 + 1 ) ∗ ( p 2 + 1 ) ∗ . . . ∗ ( p n + 1 ) (p1+1)∗(p2+1)∗...∗(pn+1) (p1+1)∗(p2+1)∗...∗(pn+1)
int phi[N],pre[N],tot;
int get(int i,int x){
int res=0;
while(i%x==0)i/=x,res++;
return res;
}
void init(){
phi[1]=1;
for(int i=2;i<N;i++){
if(!phi[i]){
pre[++tot]=i;
phi[i]=2;
}
for(int j=1;j<=tot;j++){
if(i*pre[j]>=N)break;
if(i%pre[j]==0){
int sum=get(i,pre[j]);
phi[i*pre[j]]=phi[i]/(sum+1)*(sum+2);
break;
}
phi[i*pre[j]]=phi[i]*2;
}
}
}
整除分块
已知一个整数n,计算 ∑ i = 1 n ⌊ n i ⌋ \sum_{i=1}^{n}{\left\lfloor\frac n i\right\rfloor} ∑i=1n⌊in⌋
拿到这个问题后,大脑迅速得到了响应——简单,直接暴力枚举每一个 i 计算O(n),若n很大会超时
cin>>n;
for(int L=1;L<=n;L=R+1){
R=n/(n/L);
ans+=(R-L+1)*(n/L);
cout<<L<<"-"<<R<<":"<<n/R<<enl;
}
高斯消元
有一个迷宫,有 n 个密室(编号为 1⋯n),密室之间存在一些通道。密室 n 内部布局很特殊,因此只要进入就能立刻知道;而其它密室没有任何特征,要想离开只能随机的选择一个通道口进入通道前往下一个密室。
aka 现在位于密室 1,他想前往密室 n。求需要走过的通道的数量的期望。
for(int i=1;i<=n;i++){
for(int j=n+1;j>=i;j--){ //每次把某一列除i,i外全减为0
a[i][j]/=a[i][i];
}
for(int j=1;j<=n;j++){
if(j==i)continue;
for(int k=n+1;k>=i;k--){
a[j][k]-=a[i][k]*a[j][i];
}
}
}
概率期望
**题目:**小红这天又和小紫发起了一场对战。对战的规则如下:
两人各有一些怪兽卡。每回合两人随机的从自己当前存活的怪兽卡中抽取一张发起决斗,战斗力低的怪兽卡死亡。如果两张怪兽卡战斗力相同,则无事发生。
游戏会进行到“如果抽卡,则 100% 的概率无事发生”或者“有一方卡牌被用完”时结束。请你计算小红和小紫游戏进行回合数的期望。
思路:
战斗力为2的肯定死不掉,所以这题主要看抽出战斗力为1的概率
1.先记录小红和小紫所各自拥有的战斗力为1和2的数量,如果都是2或者都是1那就输出0
2.设dp[i][j],代表小红拥有i张,小紫拥有j张的时候回合数的期望
3.设p为小红从i张战斗力为1的牌+a2张战斗力为2的牌中取出1,小紫从j张战斗力为1的牌+b2张战斗力为2的牌中取出2的概率
4.设q为小红从i张战斗力为1的牌+a2张战斗力为2的牌中取出2,小紫从j张战斗力为1的牌+b2张战斗力为2的牌中取出1的概率
5.都抽到1的概率和都抽到2的话牌数不变,所以回合数概率=(1-p-q)*dp[i][j],所以dp[i][j]=p*dp[i-1][j]+q*dp[i][j-1]+(1-p-q)*dp[i][j]+1,因为抽了一次所以最后+1
6.换算一下就是dp[i][j]=(p*dp[i-1][j]+q*dp[i][j-1]+1)/(p+q)
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=55,mod=1e9+7;
int a[3],b[3];
int dp[N][N];
int qmi(int a){
int res=1,b=mod-2;
while(b){
if(b&1)res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m,x;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>x;
a[x]++;
}
for(int i=1;i<=m;i++){
cin>>x;
b[x]++;
}
if((!a[1]&&!b[1])||(!a[2]&&!b[2])){
cout<<0<<'\n';
return 0;
}
for(int i=0;i<=n;i++){
for(int j=0;j<=m;j++){
if(i==0&&j==0)continue;
//小红抽到1,小紫抽到2的概率
int p1=(i*qmi(i+a[2])%mod)*(b[2]*qmi(j+b[2])%mod)%mod;
//小红抽到2,小紫抽到1的概率
int p2=(a[2]*qmi(i+a[2])%mod)*(j*qmi(j+b[2])%mod)%mod;
dp[i][j]=1;
if(i)dp[i][j]+=p1*dp[i-1][j]%mod;
if(j)dp[i][j]+=p2*dp[i][j-1]%mod;
dp[i][j]=dp[i][j]*qmi(p1+p2)%mod;
}
}
cout<<dp[a[1]][b[1]];
return 0;
}
隔板法
**题目:**Devu 有 N 个盒子,第 i 个盒子中有 Ai 枝花。
同一个盒子内的花颜色相同,不同盒子内的花颜色不同。
Devu 要从这些盒子中选出 M 枝花组成一束,求共有多少种方案。
若两束花每种颜色的花的数量都相同,则认为这两束花是相同的方案。
结果需对 1e9+7 取模之后方可输出。
1≤N≤20,0≤M≤1e14,0≤Ai≤1e12
思路:
隔板法:N的范围很小,M的范围很大,那么可以考虑用隔板法,在M+N-1个格子里插入N-1个隔板,即将M分成N类
容斥原理:因为每个盒子不是无数枝花,所以可以考虑用全部的排法-不满足条件的排法
1.全部的排法即C(M+N-1,N-1)
2.不满足的排法=(S1∪S2…∪SN)
3.Si表示所选的排法中这种花的数量超过了盒子中花的总数,那可知至少取了xi+1枝花,那么隔板法就应该变为从M+N-1-(xi+1)中插入N-1个隔板,即C(M+N-1-xi-1,N-1)
4.根据容斥原理奇数个加,偶数个减,就可以得到不满足的排法的总数
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=1e9+7;
int p[30];
int qmi(int a,int b){
int res=1;
while(b){
if(b&1)res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
int C(int a,int b){
if(a<b)return 0;
int d=1;
for(int i=a;i>a-b;i--)d=i%mod*d%mod;
return d;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=0;i<n;i++)cin>>p[i];
int d=1;
for(int i=1;i<n;i++)d=d*i%mod;
d=qmi(d,mod-2);
int ans=d*C(m+n-1,n-1)%mod;
for(int i=1;i<(1<<n);i++){
int x=m+n-1;
int y=1;
for(int j=0;j<n;j++){
if((i>>j)&1){
y*=-1;
x-=p[j]+1;
}
}
ans=(ans+C(x,n-1)*d%mod*y)%mod;
}
cout<<(ans+mod)%mod;
return 0;
}
求和公式
1 3 + 2 3 + . . . + a 3 = a 2 ( a + 1 ) 2 / 4 1^3+2^3+...+a^3=a^2(a+1)^2/4 13+23+...+a3=a2(a+1)2/4
1 2 + 2 2 + . . . + a 2 = a ( a + 1 ) ( 2 a + 1 ) / 6 1^2+2^2+...+a^2=a(a+1)(2a+1)/6 12+22+...+a2=a(a+1)(2a+1)/6
组合数学
排列
公式一: C a b = C a − 1 b − 1 + C a − 1 b 公式二: C n m = A n m A m m = n ( n − 1 ) ( n − 2 ) ⋅ ⋅ ⋅ ( n − m + 1 ) m ! = n ! ( n − m ) ! m ! 公式三 : C a b ≡ C a m o d p b m o d p ∗ C a / p b / p m o d p 公式一:C_a^b=C_{a-1}^{b-1}+C_{a-1}^b\\公式二:C_n^m=\dfrac{A_n^m}{A_m^m}=\dfrac{n(n-1)(n-2){···}(n-m+1)}{m!}=\dfrac{n!}{(n-m)!m!}\\公式三: C_a^b≡C_{a\mod p}^{b\mod p}*C_{a/p}^{b/p}\mod p 公式一:Cab=Ca−1b−1+Ca−1b公式二:Cnm=AmmAnm=m!n(n−1)(n−2)⋅⋅⋅(n−m+1)=(n−m)!m!n!公式三:Cab≡Camodpbmodp∗Ca/pb/pmodp
博弈论
**宗旨:**如果能走到必败点该点就是必胜点,如果只能走到必胜点那么该点就是必败点
NIM游戏
好几堆数[3,5,7],每人可以从某堆中取走任意数,谁先取完谁获胜
如果异或不为0,则第一个人可以从最大堆中取走一定的数使得其异或为0,也可以使其异或不为0;异或为0的情况无论怎么取下一次一定不为0
SG函数
SG={n>=0&&n!=SG[fa[i]]},该点的SG值=去掉所有它的后继节点的SG值后的最小非负整数
如果SG==0是必败点,否则是必胜点
如果图游戏G由若干子图游戏Gi组成,假设gi是Gi的SG函数值,那么图游戏G的SG值g(x1,…,xn)=g(x1)…g(xn)
bool vis[101];
int sg(int p){
for(int i=1;i<=k;i++){ //总共有k总拿法从小到大排序
int x=p-a[i];
if(x<0)break; //如果<0说明已经比这个数大了
if(f[x]==-1)f[x]=sg(x); //如果还没有遍历过x则遍历否则不再遍历
vis[f[x]]=true;
}
for(int i=0;;i++){ //寻找最小的不在后缀子树的sg非负值
if(!vis[i])return i;
}
}
int main(){
int n,m,k;
while(cin>>k){
for(int i=1;i<=k;i++)cin>>a[i];
sort(a+1,a+1+k);
memset(f,-1,sizeof f);
cin>>n;
while(n--){
s=0;
cin>>m;
while(m--){
int x;
cin>>x;
if(f[x]==-1)f[x]=sg(x); //记忆化
s=s^f[x];
}
if(s==0)cout<<"L"; //如果异或值为0输
else cout<<"W";
}
}
return 0;
}
字符串
进制哈希
字符串哈希把不同的字符串映射成不同的整数
p通常取质数131或13331,M通常取大整数^64,把哈希函数值h定义为ULL
1.求一个哈希值相当于求前缀和
求一个字符串的子串的哈希值相当于求区间和
2.前缀和h[i]=h[i-1]*p+s[i],h[0]=0
A B C D E 即 h [ 5 ] = A ∗ P 4 + B ∗ P 3 + C ∗ P 2 + D ∗ P 1 + E = h [ 4 ] ∗ p + E ABCDE即h[5]=A*P^4+B*P^3+C*P^2+D*P^1+E=h[4]*p+E ABCDE即h[5]=A∗P4+B∗P3+C∗P2+D∗P1+E=h[4]∗p+E
3.区间查询h[l,r]=h[r]-h[l-1]*p^(r-l+1)
例如 D E = A B C D E − A B C ∗ P 2 = D ∗ P 1 + E 即 h [ 4 , 5 ] = h [ 5 ] − h [ 3 ] ∗ p 2 DE=ABCDE-ABC*P^2=D*P^1+E即h[4,5]=h[5]-h[3]*p^2 DE=ABCDE−ABC∗P2=D∗P1+E即h[4,5]=h[5]−h[3]∗p2
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ULL unsigned long long
const int N=1e5+5,P=133;
ULL h[N],p[N];
ULL query(int l,int r){
return h[r]-h[l-1]*p[r-l+1];
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m;
cin>>n>>m;
string s;
cin>>s;
p[0]=h[0]=1;
for(int i=1;i<=n;i++){
h[i]=P*h[i-1]+s[i-1];
p[i]=P*p[i-1];
}
while(m--){
int a,b,c,d;
cin>>a>>b>>c>>d;
if(query(a,b)==query(c,d))cout<<"Yes\n";
else cout<<"No\n";
}
return 0;
}
字典树Trie
插入操作,每次从字符串第一个字符开始遍历,如果当前字典树没有存这个字符,则增加进去编号为++idx,再往下遍历,整个插入就像一棵树,与根节点相连的每种字符最多出现一次
void insert(string s){
int p=0;
for(int i=0;i<s.size();i++){
int u=s[i]-'a';
if(!son[p][u])son[p][u]=++idx;
p=son[p][u];
}
cnt[p]++;
}
查询操作,每次也是从第一个字符开始遍历,如果当前字典树没有这个字符,说明没存,存了的话就向下遍历
int query(string s){
int p=0;
for(int i=0;i<s.size();i++){
int u=s[i]-'a';
if(!son[p][u])return 0;
p=son[p][u];
}
return cnt[p];
}
存二进制
**题目:**在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
可以把每个数都存入,再找的时候,从最大位开始找,如果Ai的第i位为1,找有没有其他数这个位是0的,如果是0,就可以异或得到1,否则为0
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 search(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;
p=son[p][!s];
}
else p=son[p][s];
}
return res;
}
KMP
如果字符串是aaabaaabaaaaaa的话当匹配到第二个b位置上时候
j回溯到模式串中第三个a,如果模式串中第四个字符与字符串当前位置上的字符相同时,继续往后匹配
如果不相同则再往前回溯直到回溯到模式串最前面
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int ne[N];
signed main(){
string p,s;
int n,m;
cin>>n>>p>>m>>s;
p='0'+p;
s='0'+s;
for(int j=0,i=2;i<=n;i++){
while(j&&p[i]!=p[j+1])j=ne[j];
if(p[i]==p[j+1])j++;
ne[i]=j;
}
bool flag=false;
for(int j=0,i=1;i<=m;i++){
while(j&&p[j+1]!=s[i])j=ne[j];
if(p[j+1]==s[i])j++;
if(j==n){
if(flag)cout<<' ';
flag=true;
cout<<i-n;
j=ne[j];
}
}
return 0;
}
AC自动机
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
struct node{
int son[26];
int end;
int fail;
}t[N]; //结构体长度=单词数量*单词长度
int cnt;
void Insert(string s){ //插入单词操作
int now=0;
for(int i=0;i<s.size();i++){
int ch=s[i]-'a';
if(t[now].son[ch]==0)
t[now].son[ch]=cnt++;
now=t[now].son[ch];
}
t[now].end++;
}
void getFail(){ //寻找fail指针操作
queue<int>q;
for(int i=0;i<26;i++)
if(t[0].son[i])
q.push(t[0].son[i]);
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=0;i<26;i++){
if(t[now].son[i]){
t[t[now].son[i]].fail=t[t[now].fail].son[i];
q.push(t[now].son[i]);
}else t[now].son[i]=t[t[now].fail].son[i];
}
}
}
void query(string s){
int ans=0;
int now=0;
for(int i=0;i<s.size();i++){
int ch=s[i]-'a';
now=t[now].son[ch];
int tmp=now;
while(tmp&&t[tmp].end!=-1){
ans+=t[tmp].end;
t[tmp].end=-1;
tmp=t[tmp].fail;
}
}
cout<<ans<<'\n';
}
string str;
int main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int k;
cin>>k;
while(k--){
memset(t,0,sizeof t);
cnt=1;
int n;
cin>>n;
while(n--){
cin>>str;
Insert(str);
}
getFail();
cin>>str;
query(str);
}
return 0;
}
哈希表/散列表
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mxn=1e5+10;
ll n,price[mxn],ht[mxn],m,i;
char s[mxn][35],tmp[35],sm[]="memory";
ll BKDRHash(char *str){
ll seed=31,k=0;
while(*str)k=k*seed+*str,str++;
return k& 0x7fffffff;
}
ll find(char *str){
ll p=BKDRHash(str)%mxn;
while(ht[p] && strcmp(s[ht[p]],str)!=0)p+=2;
return p;
}
int main(){
while(cin>>n){
memset(ht,0,sizeof(ht));
memset(price,0,sizeof(price));
for(i=1;i<=n;i++)cin>>s[i],ht[find(s[i])]=i;
cin>>m;
while(m--){
ll rank=1,pri;
for(i=1;i<=n;i++){
cin>>pri>>tmp;
price[ht[find(tmp)]]+=pri;
}
for(i=1;i<=n;i++)if(price[i]>price[ht[find(sm)]])rank++;
cout<<rank<<'\n';
}
}
return 0;
}
最小表示法
1.如果str[i+k]==str[j+k] k++。
2.如果str[i+k] > str[j+k] i = i + k + 1,即最小表示不可能以str[i->i+k]开头。
3.如果str[i+k] < str[j+k] j = j + k + 1,即最小表示不可能以str[j->j+k]开头。
Manacher
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int p[N];
char s[N];
void solve(string str){
int n=str.size()*2+1;
int r=-1,c=-1,tot=0;
for(int i=0;i<n;i++)s[i]=i%2?str[tot++]:'#';
for(int i=0;i<n;i++){
p[i]=r>i?min(r-i,p[c*2-i]):1;
while(i-p[i]>=0&&i+p[i]<n){
if(s[i+p[i]]==s[i-p[i]])p[i]++;
else break;
}
if(i+p[i]>r){
c=i;
r=i+p[i];
}
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
string s;
while(cin>>s){
cout<<solve(s)<<'\n';
}
return 0;
}
语法基础
字符串的函数用法
1.查找函数find
string
a.find(b) 查找字符串s首次出现的下标(从0开始)
例如a=“touch”,b=“ou”,则a.find(b)==1;
可以用a.find(b)==-1或者a.find(b)==a.npos来判断字符串a中是否包含字符串b
其他用法:
1.s.find(str,pos)
find(str,pos)是从下标pos开始找str的位置
2.s.find_first_of(str) 和 s.find_last_of(str)
用来找目标字符在字符串中第一次出现和最后一次出现的位置
3.s.rfind(str):是从字符串右侧开始匹配str,并返回在字符串中的下标位置;
rfind(str,pos):从pos开始,向前查找匹配的字符串;
#include <bits/stdc++.h>
using namespace std;
string s,sl,sr;
int n,l,r;
int main(){
cin>>s>>n;
while(n--){
cin>>l>>r>>sl>>sr;
string sf=s.substr(0,l-1)+s.substr(r);
string st=s.substr(l-1,r-l+1);
int m=sf.find(sl),l=sl.size(),r=sr.size();
while(m!=-1&&sf.substr(m+l,r)!=sr)
m=sf.find(sl,m+1);
if(m!=-1)s=sf.substr(0,m+l)+st+sf.substr(m+l);
else s=sf+st;
}
cout<<s;
return 0;
}
char
char *m=strstr(sf,sl); //返回在 sf 中第一次出现 sl 字符串的位置,如果未找到则返回 null。
2.复制子字符串
string
s.substr(pos,len) 拷贝从下标pos开始的len的字符
如果不加参数len如s.substr(pos)则拷贝从下标pos开始后面整个字符串
char
strcat(sl,sr); //将字符串sr追加到sl后面
3.字符串长度
string
s.length()和s.size()返回字符串s的字符个数,效果相同
char
strlen(c);
4.字符串比较
可以直接比较也可以用sort函数,如string a,b; a<b;
比较规则:从首字符开始比较,小的排前面(数字字符 < 大写字母 < 小写字母)
5.输入字符串getline(cin, s)
while(getline(cin, s)){}
可以用来判断是否输入了字符串,该输入包含空格,直到'\n'结束
6.字符大小写转换tolower/toupper
大写字母变小写
for(int i=0;i<str.length();i++){
str[i]=tolower(str[i]);
}
小写字母变大戏
for(int i=0;i<str.length();i++){
str[i]=toupper(str[i]);
}
7.数字常量转换为字符串to_string
string s;
for(int i=1;i<=n;i++){
s+=to_string(i);
}
8.翻转字符串
reverse(str.begin(), str.end());
map函数用法
1.建立
unordered_map<数据类型,数据类型>mp; 查找快,占用空间小
map<数据类型,数据类型>mp;
2.查找
mp.count(x);
3.清空
mp.clear();
4.删除一个元素
mp.erase(元素名);
5.大小
mp.size();
杂项
空间复杂度
数组是5000x5000这么大,就是5000x5000x4bytes,5000x5000x4/1024/1024=95MB,这题的空间是168MB。
C类型 | 64位 | 整数范围 |
---|---|---|
char | 1字节 | / |
short int | 2字节 | 2的14次 |
int | 4字节 | 2的31次 |
long long | 8字节 | 2的63次 |
float | 4字节 | / |
double | 8字节 | / |
8位1字节,1MB=1024KB,1KB=1024B即1024字节
基本数据存储单位
1 KB ≈ 1024 B
1 MB = 1024KB
K是KB的简写
日期问题
闰年是(能被4整除但不能被100整除)或者(能被400整除)
闰年二月份29天
非闰年各月份天数=31,28,31,30,31,30,31,31,30,31,30,31
判断日期问题要考虑:
1.年份是否在所给范围以内
2.月份是否在1-12之间
3.天数是否在1-day[月]以内
bool check(int y,int m,int d){
int x=y*10000+m*100+d;
if(x<a||x>b)return false;
if(y%4==0&&y%100!=0||y%400==0)day[2]=29;
else day[2]=28;
if(m<1||m>12)return false;
if(d<1||d>day[m])return false;
return true;
}
输出小数点
cout<<fixed<<setprecision(6)<<x;
输出二进制中1的个数
cout<<__builtin_popcount(x)<<' ';
注意
1.要考虑到答案可能是负数的情况给ans赋初值
2.日期问题,考虑年月日是否都合法
3.从小到大排序,可以再多判一下是否输出重复
4.树状数组用的时候要考虑存入的值是否>0,=0会死循环
5.sqrt、sqrtl 和 sqrtf
sqrtl()函数
此功能用于长双精度型数据。因此,这将返回long double类型的平方根。这是精度更高的两倍。当我们使用长整数时,此函数很有用。