2023上半年学习笔记
笔记
可能有问题,请各位dalao多多指教。
1.图论笔记
1.1存图
1.1.1 邻接矩阵
for(int i=1;i<=n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
f[u][v]=1; //建边u->v
}
1.1.2 邻接表
vector<int>e[N];
for(int i=1;i<=n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
e[u].push_back(v);
}
1.1.3 链式前向星
struct Edge{
int nxt,to,from;
}e[500005];
void add(int u,int v)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].from=u;
head[u]=edgenum;
}
主函数部分
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
}
1.2图的遍历
DFS
void dfs(int x,int y)
{
if(a[x]!=0) return;
a[x]=y;
for(int i=0;i<e[x].size();i++)
{
dfs(e[x][i],y);
}
}
1.3最短路
1.3.1 Floyd
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
dp[i][j]=min(dp[i][k]+dp[k][j],dp[i][j]);
}
}
}
1.3.2 dijkstra及堆优化
dijkstra算法思想:从源点s开始,每次新扩展一个距
离最近的点,再以这个点为中间点,更新起点到其它点
的距离。
void dijkstra()
{
for(int i=1;i<=n;i++)d[i]=INF;
dis[1]=0;
for(int i=1;i<=n;i++)
{
int mark=-1;
mindis=INF;
for(int k=1;k<=n;k++)
{
for(int j=head[i];j;j=edge[j].nxt)
{
if(!vis[edge[j].to]&&edge[j].dis<mindis)
{
mindis=edge[j].dis;
mark=edge[j].to;
}
}
}
vis[mark]=1;
for(int i=head[mark];i;i=edge[i].nxt)
{
if(!vis[edge[i].to])
{
d[edge[i].to]=min(d[edge[i].to],d[mark]+edge[i].dis);
}
}
}
}
堆优化
struct node{
int x,val;
bool operator<(const node &tmp)const{
return val>tmp.val;
}
};
priority_queue<node> q;
void dijkstra()
{
while(!q.empty())
{
int x=q.top().x;
int val=q.top().val;
q.pop();
if(v[x]==0)
{
s1[x]=val;
v[x]=1;
for(int i=0;i<a[x].size();i++)
{
if(v[a[x][i].first]==0)
{
q.push((node){a[x][i].first,val+a[x][i].second});
}
}
}
}
}
1.3.3 Bellman-ford算法及其队列优化SPFA
Bellman-ford算法也是求解单源最短路问题,但是可
处理边权为负值的情况,同时可以判断图中是否存在负
权环(环的权值和为负数)
Bellman-ford
bool bellman(int v0)
{
for(int i=1;i<=nv;i++)
{
d[i]=INF;
}
d[v0]=0;
for(int i=1;i<nv;i++)
{
for(int j=1;j<=ne;j++)
{
if(d[edge[j].a]+edge[j].w<d[edge[j].b])
{
d[edge[j].b]=d[edge[j].a]+edge[j].w;
}
}
}
for(int j=1;j<=ne;j++)
{
if(d[edge[j].a]+edge[j].w<d[edge[j].b])
{
return 0;
}
}
return 1;
}
SPFA
void spfa()
{
int s,t;
scanf("%d%d",&s,&t);
queue<int>q;
q.push(s);
d[s]=0;
inq[s]=1;
while(!q.empty())
{
int now=q.front();
q.pop();
if(++t[now]==n)
{
f=1;
break;
}
inq[now]=0;
for(int i=0;i<e[now].size();i++)
{
int v=e[now][i].first;
if(d[v]>d[now]+e[now][i].second)
{
d[v]=d[now]+e[now][i].second;
if(inq[v]==1) continue;
inq[v]=1;
q.push(v);
}
}
}
}
1.3.4 对比
Floyd | dijkstra | Bellman-Ford | SPFA | |
---|---|---|---|---|
时间 | O(n^3) | O( (n+m)logn) | O(nm) | O(nm) |
空间 | O(n^2) | O(m) | O(m) | O(m) |
适用情况 | 稠密图,和顶点关系密切 | 稠密图,和顶点关系密切 | 稀疏图,和边关系密切 | 稀疏图,和边关系密切 |
负权 | 可以 | 不能 | 可以 | 可以 |
判负环 | 不能 | 不能 | 可以 | 可以 |
1.4 最小生成树
定义:在一个带权的无向连通图中,各边权和最小的一棵生成树即为原图的最小生成树
1.4.1 Prim
经过n次如下步骤操作:
- 1.选择一个未标记的点k,并且d[k]的值是最小的标记点k进入集合Va
- 2.以k为中间点,修改未标记点j,即Vb中的点到Va的距离值
得到最小生成树
void prim(int v0)
{
int minn;
for(int i=1;i<=n;i++) dis[i]=INF;
dis[v0]=0;
for(int i=1;i<+n;i++)
{
minn=INF;
for(int j=1;j<<=n;j++)
{
if(vis[j]==0&&minn>dis[j])
{
minn=dis[j];
k=j;
}
}
vi[k]=1;
ans+=minn;
for(int j=1;j<=n;j++)
{
if(vis[j]==0&&dis[j]>g[k][j])
{
dis[j]=g[k][j];
}
}
}
}
1.4.2 kruskal
- 首先将边按权值排序,每次从剩下的边集中选择权值最小的且与不前面所选边构成环的边加入生成树中
- 直到加入了n-1条边
- 判断环用并查集:待加边的两个端点若已在生成树中,则必定构成环
int find(int x)//并查集
{
if(f[x]!=x) f[x]=find(f[x]);
return f[x];
}
kruskal部分
for(int i=1;i<=m;i++)
{
if(find(e[i].x)!=find(e[i].y))
{
cnt++;
ans+=e[i].z;
f[find(e[i].x)]=find(e[i].y);
}
if(cnt==n-1)
break;
}
1.5 倍增求LCA(最近公共祖先)
- 在调整游标的第一阶段中,我们要将u、v两点跳转到同一深度。我们可以计算出u、v两点的深度之差,设其为y。通过将y进行二进制拆分,我们将y次游标跳转优化为y的二进制表示所含1的个数”次游标跳转。
- 在第二阶段中,我们从最大的i开始循环尝试,一直尝试到0(包括0),如果\(fa[u][i]!=fa[v][i]\),则\(u=fa[u][i]\),\(v=fa[v][i]\)那么最后的LCA为\(fa[u][0]\)。
void dfs(int u,int fa)//预处理
{
dep[u]=dep[fa]+1;
for(int i=0;i<=19;i++)
{
f[u][i+1]=f[f[u][i]][i];
}
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
f[v][0]=u;
dfs(v,u);
}
}
int lca(int x,int y)
{
if(dep[x]<dep[y])
swap(x,y);
for(int i=20;i>=0;i--)//第一阶段
{
if(dep[f[x][i]]>=dep[y]) x=f[x][i];
if(x==y) return x;
}
for(int i=20;i>=0;i--)//第二阶段
{
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
1.6 拓扑排序
补充知识:
DAG:有向无环图
- 在一个DAG中,一条有向边的起点叫做其终点的前驱结点,同理,终点是起点的后继结点
拓扑排序过程
- 选择一个入度为0的顶点并输出
- 然后从DAG中删除此顶点以及其关联边
- 重复上述两步,直到不存在入度为0的顶点为止
void topo()
{
while(!q.empty())
{
int x=q.front();
//printf("%d %d\n",x,ans);
q.pop();
for(int i=1;i<=n;i++)
{
if(f[x][i]==1)
{
r[i]--;
if(r[i]==0)
{
q.push(i);
a[i]=a[x]+1;
if(ans<a[x]+1)
ans=a[x]+1;
}
}
}
}
}
1.7 强连通分量(SCC)
补充知识:
- 强连通:在一个有向图G中,如果两个顶点u、v间存在一条u到v的路径且也存在一条v到u的路径,则称这两个顶点u、v是强连通的
- 强连通图:有向图G的任意两个顶点都强连通,则称G是一个强连通图
- 极大强连通子图:G是一个极大强连通子图,当且仅当G是一个强连通子图且不存在另一个强连图子图G’ G
- 强连通分量:有向非强连通图的极大强连通子图,称为强连通分量
定义dfn(u)为节点u搜索的次序编号(时间戳),low(u)为u或u的子树能够追溯到的最早(最先)的栈中节点的次序号
void tarjan(int i)
{
dfn[i]=low[i]=++num;
s[++top]=i;
ins[i]=1;
for(int u=head[i];u;u=e[u].nxt)
{
int j=e[u].to;
if(!dfn[j])
{
tarjan(j);
low[i]=min(low[i],low[j]);
}
else if(ins[j])
{
low[i]=min(low[i],dfn[j]);
}
}
int v=0;
if(low[i]==dfn[i])
{
scc++;
while(v!=i)
{
v=s[top];
top--;
ins[v]=0;
belong[v]=scc;
}
}
}
2.动态规划(DP)
2.1 区间DP
- 状态:用\(dp[i][j]\)表示区间(\(i,j\))的最优解
- 状态转移最常见的写法:
- \(dp[i][j]=max/min(dp[i][j],dp[i][k]+dp[k+1][j]+sth)\)
- 理解:区间(\(i,j\))的最优解就等于区间(\(i,j\))同(\(i,k\))和区间(\(k+1,j\))合并后的值比谁更优。
- 初始化: \(dp[i][i]=0/other\)
for(int len=2;len<=n;len++)
{
for(int l=1;l+len-1<=n;i++)
{
int r=l+len-1;
for(int k=l;k<=r;k++)
{
dp[l][r]=max/min(dp[l][r],dp[l][k]+dp[k+1][r]+sth);
}
}
}
2.2 状压DP
状压DP具体的做法是将某一个状态通过二进制01压缩成一个数进行存储
例题:\(P1896\) 互不侵犯
国王的放置状态只与上一行的状态与本行前面的状态有关,因此,我们用01串来表示某一行的国王放置状态,1表示放,0表示不放,这样,国王放置的状态就可以压缩成一个数对于相邻两行,我们只需要进行一下与运算,如果有值,就说明不合法
#include<cstdio>
using namespace std;
int n,m,tot;
int a[520],b[520];
long long int f[10][520][205];
int get(int x)//求当前状态中1的数量
{
int sum=0;
while(x){
++sum;
x&=x-1;
}
return sum;
}
int main()
{
scanf("%d%d",&n,&m);
int tmp=(1<<n)-1;
for(int i=0;i<=tmp;i++)
{
if(i&(i<<1)) continue;//判断是否有相邻的棋子
a[++tot]=i;
b[tot]=get(i);
f[1][tot][b[tot]]=1;//第一行要单独初始化
}
for(int i=2;i<=n;i++)
{
for(int j=1;j<=tot;j++)
{
for(int k=1;k<=tot;k++)
{
if(a[j]&a[k]) continue;
if(a[j]&a[k]<<1) continue;
if(a[k]&a[j]<<1) continue;//判断是否与前一行冲突
for(int l=0;l<=m;l++)
{
f[i][j][b[j]+l]+=f[i-1][k][l];
}
}
}
}
long long ans=0;
for(int i=1;i<=tot;i++)
{
ans+=f[n][i][m];
}
printf("%lld",ans);
return 0;
}
2.3 数位DP
有一类与数位有关的区间统计问题,我们可以用dp的思
想,以数位为阶段,在数位上进行递推,这就是数位dp
例如:求给定区间中,满足给定条件的某个D进制数或
此类数的数量。
解决这类问题的基本思想就是逐位确定,有时求解这类
问题还需要做预处理,预处理的过程也可以看成数位DP
例题:\(P2602\) 数字计数
由于\(1≤a≤b≤10\)^\(12\) ,数据范围较大,不能逐个拆解,因此用数位DP
- 发现对于满i位的数,所有数字出现的次数都是相同的,故设数组dp[i]为满i位的数中每个数字出现的次数,此时暂时不处理前导零
- 则有 \(dp[i]=10*dp[i-1]+10\)^\((i-1)\);
- 将上界按位分开,从高到低枚举,不贴着上界时,后面可以随便取值。贴着上界时,后面就只能取0到上界,分两部分分别计算贡献
- 最后考虑前导零,第i位为前导0时,此时1到i-1位也都是0,也就是多算了将i-1位填满的答案,需要额外减去
#include<cstdio>
using namespace std;
long long x,b,dp[15],p[15],ans[15][2];
long long a[20];
void solve(long long n)
{
long long int tmp=n;
int len=0;
while(n)
{
len++;
a[len]=n%10;
n/=10;
}
for(int i=len;i>0;i--)
{
for(int j=0;j<10;j++)
ans[j][0]+=dp[i-1]*a[i];
for(int j=0;j<a[i];j++)
ans[j][0]+=p[i-1];
tmp-=a[i]*p[i-1];
ans[a[i]][0]+=tmp+1;
ans[0][0]-=p[i-1];
}
}
int main()
{
scanf("%lld%lld",&x,&b);
p[0]=(long long)1;
for(int i=1;i<=13;i++)
{
p[i]=p[i-1]*(long long)10;
dp[i]=dp[i-1]*10+p[i-1];
}
solve(b);
for(int i=0;i<=9;i++)
{
ans[i][1]=ans[i][0];
ans[i][0]=0;
}
solve(x-1);
for(int i=0;i<=9;i++)
{
printf("%lld ",ans[i][1]-ans[i][0]);
}
return 0;
}
2.4 斜率优化DP
有一类问题:将n个物品分为连续的若干份(不限制份数),每分一次会产生一个代价(告诉你代价的计算方式),问代价最多/最小是多少
容易想到,可以用线性dp来解决,令dp[i]表示选择前i个物品的最大/最小代价是多少,则:
dp[i]=max/min{dp[j]+代价公式|1≤j<i}
很明显,这个朴素的dp时间复杂度为O(n^2),而单调队列优化的是定长区间的取最大或最小问题,不适于本类型
能否在O(1)时间内找到所有转移里最优的那个呢?
请看例题
例题: \(P3628\) 特别行动队
注:s[]为前缀和
公式: \(dp[i]=max{dp[j]+a(s[i]-s[j])^2+b(s[i]-s[j])+c}\)
如果要使从j转移比从k转移更优
则\(dp[j]+a(s[i]-s[j])^2+b(s[i]-s[j])+c>dp[k]+a(s[i]-s[k])^2+b(s[i]-s[k])+c\)
不难发现,上述不等式可化简为\((dp[j]-dp[k]+a *s[j]^2-a*s[k]^2+b*s[j]-b*s[k])/(s[j]-s[k])>2a*s[i]\)
此时,令\(dp[j]+a*s[j]^2+b*s[j]=f[j]\)
\(dp[k]+a*s[k]^2+b*s[k]=f[k]\)
则原式=\((f[j]-f[k])/(s[j]-s[k])>2a*s[i]\)
由此,可想到斜率公式,因此,只用使斜率单调递减即可,此处用单调队列维护
#include<cstdio>
using namespace std;
long long int n,dp[1000005],s[1000005],a,b,c;
int q[1000005],head,tail;
long long int getDP(int i,int j)
{
return dp[j]+a*(s[i]-s[j])*(s[i]-s[j])+b*(s[i]-s[j])+c;
}
long long int getUP(int i,int j)
{
return dp[i]+a*s[i]*s[i]-b*s[i]-dp[j]-a*s[j]*s[j]+b*s[j];
}
long long int getDOWN(int i,int j)
{
return s[i]-s[j];
}
int main()
{
scanf("%lld",&n);
scanf("%lld%lld%lld",&a,&b,&c);
for(int i=1;i<=n;i++)
{
scanf("%lld",&s[i]);
s[i]+=s[i-1];
}
tail++;
for(int i=1;i<=n;i++)
{
while(head+1<tail&&getUP(q[head+1],q[head])>s[i]*2*a*getDOWN(q[head+1],q[head]))
head++;
dp[i]=getDP(i,q[head]);
while(head+1<tail&&getUP(i,q[tail-1])*getDOWN(q[tail-1],q[tail-2])>=getDOWN(i,q[tail-1])*getUP(q[tail-1],q[tail-2]))
tail--;
q[tail++]=i;
}
printf("%lld",dp[n]);
return 0;
}
3.数据结构
3.1 单调栈
单调栈,是一种特殊的栈,特殊之处在于栈内的元素都保持一个单调性
为了维护单调栈的单调性,当某元素入栈时,有可能会将某些元素弹栈
例如,某个单调栈具有单调递增性,当要入栈的元素比栈顶元素小,则必须将栈顶元素弹栈
3.2 单调队列
单调队列分两种,一种是单调递增的,另外一种是单调递减的
用单调队列来解决问题,一般都是需要得到当前的某个范围内的最小值或最大值
3.3 优先队列
是一棵完全二叉树,数组存储
树中每个节点与数组中存放该节点中的值的那个元素相对应
也可以称为堆
优先队列的stl操作
• empty( ) 如果队列为空,则返回真
• pop( ) 删除堆顶元素,即删除第一个元素
• push( ) 加入一个元素
• size( ) 返回优先队列中拥有的元素个数
• top( ) 返回优先队列中堆顶元素
3.4 树状数组
一个长度为n的序列a[1] ~ a[n]
修改某个元素的值,再次询问前缀和
假设有m次修改,对于每次修改后的询问,暴力求解时间复杂度为O(n)
当\(n,m>=10000\)时,就会超时
暴力求解慢的原因在哪里?
就是每次求前缀和时,重复计算!
如果把前缀固定分解成若干个区间,每次修改某个元素的时候,只影响其所在区间,其它区间还是保持原值,就会提高求前缀和的效率。
区间划分:
根据任意正整数的关于2的不重复次幂的唯一分解性质,
即任意正整数只有唯一的二进制表示形式
若一个正整数x可以被“二进制分解”,则区间[1,x]可以分成O(logx)个小区间
这些小区间的共同特点是:若区间结尾为r,则区间长度就等于r的“二进制分解”下最小的2的次幂,也就是最后一个1的位置的权值,即lowbit(r)
例如,\(x=7=2^2+2^1+2^0\),那么区间[1,7]就可以分成[1,4]、[5,6]、[7,7]三个小区间,长度分别为lowbit(4)=4、lowbit(6)=2、lowbit(7)=1
此时,每次修改和查询,只涉及O(logx)个区间,降低了时间复杂度
性质
- 树状数组中某个节点i的父节点的下标为i+lowbit(i)
- 树状数组中某个节点i的兄弟(左边的)下标为i-lowbit(i)。
例题:\(P3374\)
#include<cstdio>
using namespace std;
int c[500005],a[500005],n,m;
int lowbit(int k)
{
return k & -k;
}
void add(int x,int k)//单点修改
{
while(x<=n)
{
c[x]+=k;
x+=lowbit(x);
}
}
int sum(int x)//区间求和
{
int ans=0;
while(x>0)
{
ans=ans+c[x];
x-=lowbit(x);
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
add(i,a[i]);
}
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
if(x==1)
{
add(y,z);
}
else
{
printf("%d\n",sum(z)-sum(y-1));
}
}
return 0;
}
3.5 线段树
线段树的本质是一棵二叉树,不同于其它二叉树,线段树的每一个节点记录的是一段区间的信息
创建树的节点
const int maxn=100000;
struct seg{
int l,r;
int lazy,sum;
}tr[maxn<<2];
计算左右儿子的编号
#define lid (id<<1)
#define rid (id<<1|1)
建树
void build(int id,int l,int r)
{
tr[id].l=l;
tr[id].r=r;
if(l==r)
{
tr[id].sum=a[l];
return;
}
int mid=(l+r)>>1;
build(lid,l,mid);
build(rid,mid+1,r);
tr[id].sum=tr[lid].sum+tr[rid].sum;
}
区间修改
将标记lazy打在区间上,表示该区间会有一个lazy的增加如果在之后的维护或查询过程中,需要对这个结点递归地进行处理,则当场将这个标记分解,传递给它的两个子结点
void add(int id,int l,int r,int val)
{
pushdown(id);
if(tr[id].l==l&&tr[id].r==r)
{
tr[id].lazy+=val;
tr[id].sum+=val*(tr[id].r-tr[id].l+1);
return;
}
int mid=(tr[id].l+tr[id].r)>>1;
if(r<=mid) add(lid,l,r,val);
else if(l>mid) add(lid,l,mid,val);
else add(lid,l,mid,val),add(rid,mid+1,r,val);
tr[id].sum=tr[lid].sum+tr[rid].sum;
}
下放标记
void pushdown(int id)
{
if(tr[id].lazy&&tr[id].l!=tr[id].r)
{
tr[lid].lazy+=tr[id].lazy;
tr[rid].lazy+=tr[id].lazy;
tr[lid].sum+=tr[id].lazy*(tr[lid].r-tr[lid].l+1);
tr[rid].sum+=tr[id].lazy*(tr[rid].r-tr[rid].l+1);
tr[id].lazy=0;
}
}
区间查询
int query(int id,int l,int r)
{
pushdown(id);
if(tr[id].l==l&&tr[id].r==r)
{
return tr[id].sum;
}
int mid=(tr[id].l+tr[id].r)>>1;
if(mid>=r) return query(lid,l,r);
else if(mid<=l) return query(rid,l,r);
else return query(lid,l,mid)+query(rid,mid+1,r);
}