int x,y,w;
cin>>x>>y>>w;
Edge e={y,w};
Adj[x].push_back(e);
}
Dijkstra(0);
for(int i=n-1;i>=0;i--) cout<
拓扑排序
AOV网
一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的,但有些子工程没有先决条件,可以安排在任何时间开始。为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。
AOV是一个DAG(有向无环图)。
拓朴排序概念
由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
拓扑排序求法
由AOV网构造拓扑排序的步骤主要是循环执行以下两步,直到不存在入度为0的点为止。
- 寻找一入度为0的点并输出之;
- 删除该点及其所有出边。
拓扑排序用来确定一个依赖关系集中事物发生的顺序。
代码:
用来判断一个图是否是有向无环图
vector G[MAXV];//邻接表
int n,m,inDegree[MAXV];//顶点数、入度
//拓扑排序
bool topologicalSort(){
int num=0; //记录加入拓扑序列的顶点数
queue q;
//将所有入度为0的点压入q
for(int i=0;i
if(inDegree[i]==0){
q.push(i);
}
}
while(!q.empty()){
int u=q.front();
printf("%d ",u);
q.pop();
//将u的邻接点的入度全减1
for(int i=0;i
int v=G[u][i];
inDegree[v]--;
if(inDegree[v]==0) q.push(v);
}
//清空u的所有出边
G[u].clear();
num++;
}
//加入拓扑序列的顶点数为n,说明拓扑排序成功
if(num==n) return true;
return false;
}
最小生成树
Minimum Spanning Tree,MST
设G=(V,E)是一个无向连通网,生成树上各边的权值之和为该生成树的代价,在G的所有生成树中,代价最小的生成树就称为最小支撑树,或称最小生成树。
只有连通图才有生成树,而对于非连通图,只存在生成森林。
Kruskal 算法
Kruskal 算法是一种常见并且好写的最小生成树算法,由 Kruskal 发明。该算法的基本思想是从小到大加入边,是个贪心算法。
前置知识¶
并查集、贪心、图的存储。
struct edge{
int u,v;//两个端点编号
int cost;//边权
}E[MAXE];
//按边权从小到大排序
bool cmp(edge a,edge b){
return a.cost
令最小生成树的边权之和为ans、最小生成树的当前边数Num_Edge;
将所有边按边权从小到大排序;
for(从小到大枚举所有边){
if(当前测试边的两个端点在不同的连通块中){
将该测试边加入最小生成树中;
ans+=测试边的边权;
Num_Edge++;
当Num_Edge=顶点数-1时结束循环;
}
}
return ans;
}
有两个问题:
- 如何判断测试边的两个端点是否在不同的连通块中——并查集的查询功能
- 如何将测试边加入最小生成树——并查集的合并功能
int father[N];//并查集数组
int find(int x){
return father[x]==x?x:fa[x]=find(fa[x]);
}
inline void merge(int x,int y){
fa[find(x)]=find(y);
}
//kruskal函数返回最小生成树的边权之和,参数n为顶点个数,m为图的边数
int kruskal(int n,int m){
//ans为边权之和,Num_Edge为当前生成树的边数
int ans=0,Num_Edge=0;
//并查集初始化
for(int i=1;i<=n;i++){
father[i]=i;
}
//所有边按边权从小到大排序
sort(E,E+m,cmp);
//枚举所有边
for(int i=0;i
int faU=find(E[i].v),faV=find(E[i].u);
//如果不在一个集合中
if(faU!=faV){
//合并集合
father[faU]=faV;
//边权之和增加测试边的边权
ans+=E[i].cost;
//当前生成树的边数+1
Num_Edge++;
//边数等于顶点数减1时结束算法
if(Num_Edge==n-1) break;
}
}
if(Num_Edge!=n-1) return -1;
return ans;
}
例题
蓝桥杯 通电
#include
#include
#include
#include
using namespace std;
#define MAXN 1005
typedef long long ll;
struct Village{
int x,y,h;
}V[MAXN];
struct Edge{
int u,v;
double cost;
bool operator<(const Edge&a) const{
return this->cost
return fa[x]==x?x:fa[x]=find(fa[x]);
}
double kruskal(){
for(int i=0;i
int faU=find(E[i].u),faV=find(E[i].v);
if(faU!=faV){
fa[faU]=faV;
ans+=E[i].cost;
Edge_Num++;
if(Edge_Num==n-1) break;
}
}
if(Edge_Num!=n-1) return -1;
return ans;
}
int main(){
scanf("%d",&n);
int x,y,h;
for(int i=0;i
数学知识
数论
1 分解质因数
void divide(int n){
for(int i=2;i<=n/i;i++){
if(n%i==0){
int s=0;
while(n%i==0){
n/=i;//把质因数i除尽
s++;//计数该质因数i的个数
}
cout<1) cout<
2 筛质数
2.1 试除法
bool is_prime(int n){
for(int i=2;i<=n/i;i++)
if(n%i==0) return false;
return true;
}
2.2 埃氏筛
时间复杂度O(nlog(logn))
isprime[0]=isprime[1]=false;
for(int i=2;i<=maxn;i++){
if(isprime[i]){
prime[cnt++]=i;
for(int j=i*i;j<=maxn;j+=i){
isprime[j]=false;
}
}
}
这里有一个小优化,j 从 i * i 而不是从 i + i开始,因为 i*(2~ i-1)在 2~i-1时都已经被筛去,所以从i * i开始。
对于一个合数,有可能被筛多次。例如 30 = 2 * 15 = 3 * 10 = 5*6……
用筛法求之N内的素数。 - C语言网 (dotcpp.com)
2.3 欧拉筛
for(int i=2;i<=maxn;i++){
if(IsPrime[i]){
prime[++prime[0]]=i;
}
for(int j=1;j<=prime[0]&&i*prime[j]<=maxn;j++){
IsPrime[i*prime[j]]=false;
if(i%prime[j]==0)
break;
}
}
欧拉筛保证了每个合数只被它的最小质因子筛除。
i
0
%
p
r
i
m
e
[
j
]
=
=
0
i_0 % prime[j]==0
i0%prime[j]==0保证了这一特性,理由如下,
当
i
0
=
k
∗
p
r
i
m
e
[
j
]
时
,
i
0
∗
p
r
i
m
e
[
j
+
1
]
=
p
r
i
m
e
[
j
]
∗
k
∗
p
r
i
m
e
[
j
+
1
]
当i_0=k*prime[j]时,i_0*prime[j+1]=prime[j]*k*prime[j+1]
当i0=k∗prime[j]时,i0∗prime[j+1]=prime[j]∗k∗prime[j+1],
这
说
明
后
面
当
遇
到
一
个
i
1
=
k
∗
p
r
i
m
e
[
j
+
1
]
时
,
会
筛
除
i
1
∗
p
r
i
m
e
[
j
]
=
i
0
∗
p
r
i
m
e
[
j
+
1
]
这说明后面当遇到一个i_1=k*prime[j+1]时,会筛除i_1*prime[j]=i_0*prime[j+1]
这说明后面当遇到一个i1=k∗prime[j+1]时,会筛除i1∗prime[j]=i0∗prime[j+1]。
前面的不会筛除后面会筛除的。
例子:求n以内的素数
#include
#include
using namespace std;
int prime[10000];
bitset<10000> IsPrime;
int main()
{
int n,i,j;
cin >> n;
IsPrime[0] = IsPrime[1] = 1;
for(i=2;i
if(!IsPrime[i]){
prime[++prime[0]] = i;
printf("%dn", i);
}
for (j = 1; j <= prime[0] && i * prime[j] < n; j++) {
IsPrime[i * prime[j]] = 1;
if (i % prime[j] == 0)
break;
}
}
return 0;
}
#include
#include
#include
#include
#include
using namespace std;
#define maxn 20210605
bool IsPrime[maxn];
int prime[maxn];
void oula(){
memset(IsPrime,true,sizeof(IsPrime));
IsPrime[0]=IsPrime[1]=false;
for(int i=2;i<=maxn;i++){
if(IsPrime[i]) prime[++prime[0]]=i;
for(int j=1;j<=prime[0]&&i*prime[j]<=maxn;j++){
IsPrime[i*prime[j]]=false;
if(i%prime[j]==0) break;
}
}
}
bool judge(int x){
if(!IsPrime[x]) return false;
while(x!=0){
if(!IsPrime[x%10]) return false;
x/=10;
}
return true;
}
int ans;
int main(){
clock_t starttime,endtime;
starttime=clock();
oula();
for(int i=1;i<=maxn;i++){
if(judge(i)){
printf("%d ",i);
ans++;
}
}
printf("%d",ans);
endtime=clock();
printf("n耗时:%f sn",(double)(endtime-starttime)/CLOCKS_PER_SEC);
return 0;
}
2.4 杜教筛
2.5 min_25筛
3 约数
3.1 试除法求约数
https://www.acwing.com/problem/content/871/
vector get_divisors(int x)
{
vector res;
for (int i = 1; i <= x / i; i ++ )
if (x % i == 0)
{
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
sort(res.begin(), res.end());
return res;
}
auto res=get_divisors(n);
for(auto t:res) cout<
3.2 约数个数和约数之和
KaTeX parse error: No such environment: gather* at position 8: begin{̲g̲a̲t̲h̲e̲r̲*̲}̲如果space N ={p_…
线性筛求约数个数
void pre(){
d[1]=1;
for(int i=2;i<=n;i++){
if(!v[i]) v[i]=1,p[++tot]=i,d[i]=2,num[i]=1;
for(int j=1;j<=tot&&i<=n/p[j];++j){
v[i*p[j]]=1;
if(i%p[j]==0){
num[i*p[j]]=num[i]+1;
d[i*p[j]]=d[i]/num[i*p[j]]*(num[i*p[j]]+1);
break;
}else{
num[i*p[j]]=1;
d[i*p[j]]=d[i]*2;
}
}
}
}
线性筛求约数和
3.3 最大公约数-欧几里得算法
gcd(a,b)=gcd(b,a mod b);
int gcd(int a,int b){ return b==0?a:(a=gcd(b,a%b)); }
4 欧拉函数
#欧拉函数
欧拉函数
φ
(
n
)
varphi (n)
φ(n)表示小于等于n且和n互质(gcd=1)的数的数量。
φ
(
1
)
=
1
varphi(1)=1
φ(1)=1。
当n是质数的时候,显然有
φ
(
n
)
=
n
−
1
varphi(n)=n-1
φ(n)=n−1。
欧拉函数的一些性质
- 欧拉函数是积性函数。
- 积性是指对于两个互质的整数a和b有f(ab)=f(a)
×
times
×f(b)。
- 特别地对于n是奇数时,
φ
(
2
n
)
=
φ
(
n
)
varphi(2n)=varphi(n)
φ(2n)=φ(n)。
- n=
∑
d
∣
n
φ
(
d
)
sum_{d|n}{varphi(d)}
∑d∣nφ(d)。
- 若
n
=
p
k
n=p^k
n=pk,其中p是质数,那么
φ
(
n
)
=
p
k
−
p
(
k
−
1
)
varphi(n)=p^k-p^(k-1)
φ(n)=pk−p(k−1)。
- 根据整数唯一分解定理(算术基本定理),设
φ
(
n
)
=
n
×
∏
i
=
1
s
p
i
−
1
p
i
varphi (n)=ntimes prod_{i=1}^{s}{frac{p_i-1}{p_i}}
φ(n)=n×∏i=1spipi−1.
![[Pasted image 20220524150902.png]]
如何求欧拉函数值
求一个数的欧拉函数值
根据定义质因数分解的同时求解就好。这个过程可以用Pollard Rho算法优化。
int euler_phi(int n){
int m=int(sqrt(n+0.5));
int ans=n;
for(int i=2;i<=m;i++)
if(n%i==0){
ans=ans/i*(i-1);
while(n%i==0) n/=i;
}
if(n>1) ans=ans/n*(n-1);
return ans;
}
优化
int euler_phi(int n){
int ans=n;
for(int i=2;i<=n/i;i++)
if(n%i==0){
ans=ans/i*(1-i);
while(n%i==0) n/=i;
}
if(n>1) ans=ans/n*(n-1);
return ans;
}
求多个数的欧拉函数值
线性筛(euler筛)求解欧拉函数
![[Pasted image 20220524154123.png]]
void pre(){
memset(is_prime,1,sizeof is_prime);
int cnt=0;
is_prime[1]=0;
phi[i]=1;
for(int i=2;i<=5000000;i++){
if(is_prime[i]){
prime[++cnt]=i;
phi[i]=i-1
}
for(int j=1;j<=cnt&&i*prime[j]<=5000000;j++){
is_prime[i*prime[j]]=0;
if(i%prime[j])
phi[i*prime[j]]=phi[i]*phi[prime[j]];
else{
phi[i*prime[j]]=phi[i]*prime[j];
break;
}
}
}
}
欧拉定理
若
g
c
d
(
a
,
m
)
=
1
,
则
a
φ
(
m
)
≡
1
(
m
o
d
m
)
gcd(a,m)=1,则a^{varphi (m)}equiv1(mod m)
gcd(a,m)=1,则aφ(m)≡1(modm)。
这里的
≡
equiv
≡表示同余,数学上,两个整数除以同一个整数,若得相同余数,则二整数同余。
当m为质数时,欧拉定理变为了费马定理,
此时
φ
(
m
)
=
m
−
1
varphi (m)=m-1
φ(m)=m−1,则费马定理内容为,
若
g
c
d
(
a
,
m
)
=
1
,
则
a
m
−
1
≡
1
(
m
o
d
m
)
gcd(a,m)=1,则a^{m-1}equiv1(mod m)
gcd(a,m)=1,则am−1≡1(modm)。
扩展欧拉定理
![[Pasted image 20220524160727.png]]
5 快速幂
#快速幂
a
b
a^b
ab
long long binpow(long long a,long long b){
long long res=1;
while(b>0){
if(b&1) res*=a;
a*=a;
b>>1;
}
return res;
}
a
b
%
m
a^b % m
ab%m
long long binpow(long long a,long long b,long long m){
long long res =1;
while(b>0){
if(b&1) res=res*a%m;
a=a*a%m;
b>>1;
}
return res%m;
}
矩阵快速幂
快速幂求乘法逆元
乘法逆元
若gcd(b,m)=1,b|a,则
a
b
≡
a
˙
b
−
1
(
m
o
d
m
)
frac{a}{b}equiv a dot b^{-1}(mod m)
ba≡a ˙b−1(modm)
把除法变为乘法。
上式两边同乘b得
a
≡
a
˙
b
˙
b
−
1
(
m
o
d
m
)
。
a equiv a dot b dot b^{-1}(mod m)。
a≡a ˙b ˙b−1(modm)。
两边同除以a得
b
˙
b
−
1
≡
1
(
m
o
d
m
)
b dot b^{-1} equiv 1(mod m)
b ˙b−1≡1(modm)
6 扩展欧几里得
求逆元
#include
int exgcd(int a,int b,int &x,int &y){
if(!b){
x=1;y=0;
return a;
}
int d=exgcd(b,a%b,y,x);
y-=(a/b)*x;
return d;
}
int main(){
int x,y;
exgcd(2,3,x,y);
printf("%d %d",x,y);
return 0;
}
7 中国剩余定理
中国剩余定理 (Chinese Remainder Theorem, CRT) 可求解如下形式的一元线性同余方程组,其中
{
n
1
,
n
2
,
.
.
.
,
n
k
两
两
互
质
}
{n_1,n_2,... ,n_k两两互质}
{n1,n2,...,nk两两互质}
{
x
≡
a
1
(
m
o
d
n
1
)
x
≡
a
2
(
m
o
d
n
2
)
.
.
.
x
≡
a
k
(
m
o
d
n
k
)
begin{cases} & x equiv a_{1}(mod n_1) \ & x equiv a_{2}(mod n_2) \ & . \ & . \ & . \ & x equiv a_{k}(mod n_k) end{cases}
⎩⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎧x≡a1(modn1)x≡a2(modn2)...x≡ak(modnk)
算法流程
- 计算所有模数的积n;
- 对于第i个方程:
a.计算
m
i
=
n
n
i
m_i=frac{n}{n_i}
mi=nin;
b.计算
m
i
m_i
mi在
n
i
n_i
ni意义下的逆元
m
i
−
1
{m_i}^{-1}
mi−1;
c.计算
c
i
=
m
i
m
i
−
1
c_i=m_im_i^{-1}
ci=mimi−1(不要对
n
i
n_i
ni取模)。 - 方程组的唯一解为:
x
=
∑
i
=
1
k
a
i
c
i
(
m
o
d
n
)
x=sum_{i=1}^{k}{a_ic_i(mod n)}
x=∑i=1kaici(modn)。
LL CRT(int k,LL* a,LL*r){
LL n=1,,ans=0;
for(int i=1;i<=k;i++) n*=r[i];
for(int i=1;i<=k;i++){
LL m=n/r[i],b,y;
exgcd(m,r[i],b,y);//b*m mod r[i]=1
ans=(ans+a[i]*m*b%n)%n;
}
return (ans%n+n)%n;
}
8 裴蜀定理
对于任意正整数a,b一定存在整数x,y,使得ax+by=gcd(a,b)。
可应用扩展欧几里得来算出这样的一组(x,y)。
字符串
数据结构
并查集
算法学习笔记(1) : 并查集 - 知乎 (zhihu.com)
初始化
int fa[MAXN];
inline void init(int n)
{
for (int i = 1; i <= n; ++i)
fa[i] = i;//父节点为自己
}
查询
一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。
int find(int x)
{
if(fa[x] == x)//父节点为自身,为根节点
return x;
else
return find(fa[x]);
}
合并
inline void merge(int i, int j)
{
fa[find(i)] = find(j);//把i的父节点的父节点设置为j的父节点
}
查询(路径压缩)
int find(int x)
{
if(x == fa[x])
return x;
else{
fa[x] = find(fa[x]); //父节点设为父节点的父节点,最后这条路上所有点父节点都设为根节点
return fa[x]; //返回父节点
}
}
以上代码常常简写为一行:
int find(int x)
{
return x == fa[x] ? x : (fa[x] = find(fa[x]));
}
初始化(按秩合并)
inline void init(int n)
{
for (int i = 1; i <= n; ++i)
{
fa[i] = i;
rank[i] = 1;
}
}
合并(按秩合并)
inline void merge(int i, int j)
{
int x = find(i), y = find(j); //先找到两个根节点
if (rank[x] <= rank[y])//x的高度小于等于y,则合并x那一路到y
fa[x] = y;
else
fa[y] = x;
if (rank[x] == rank[y] && x != y)//如果高度相同且根节点不同,则新的根节点的高度+1
rank[y]++;
}
树状数组
线段树
基本操作:
①pushup():由子节点算父节点
②build():将一段区间初始化为线段树,若区间长度为n,则存储线段树的一维空间长度建议为4*n。
③modify():修改,分为对单点的修改和对区间的修改,其中对区间修改涉及pushdown()操作函数和懒标记。
④query():查询某段区间
对于编号为x的结点:其父节点为x>>1,左儿子为x<<1,右儿子为x<<1|1。
单点修改
区间最大值查询
struct Node{
int l,r;
int v;
}tr[MAXN*4];
void build(int u,int l,int r){
tr[u]={l,r};
if(l==r) return;
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
void pushup(int u){
tr[u]=max(tr[u<<1],tr[u<<1|1]);
}
int query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].v;
int mid=tr[u].l+tr[u].r>>1;
int v=0;
if(l<=mid) v=query(u<<1,l,r);
else v=max(v,query(u<<1|1,l,r));
return v;
}
void modify(int u,int x,int v){
if(tr[u].l==x&&tr[u].r==x) tr[u].v=v;
else{
int mid=tr[u].l+tr[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
区间最大连续子段和查询
int w[MAXN];//数列
struct Node{
int l,r;
int sum,lmax,rmax,tmax;
}tr[MAXN*4];
void pushup(Node &u,Node &l,Node &r){
u.sum=l.sum+r.sum;
u.lmax=max(l.lmax,l.sum+r.lmax);
u.rmax=max(r.rmax,r.sum+l.rmax);
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u){
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u=1,int l=1,int r=n){
if(l==r) tr[u]={l,r,w[r],w[r],w[r],w[r]};
else{
int mid=tr[u].l+tr[u].r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
int modify(int u=1,int x,int v){
if(tr[u].l==x&&tr[u].r==x) tr[u]={x,x,v,v,v};
else{
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
Node query(int u=1,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
else{
int mid=tr[u].l+tr[u].r>>1;
if(r<=mid) return query(u<<1,l,r);
else if(l>mid) return query(u<<1|1,l,r);
else{
auto left=query(u<<1,l,r);
auto right=query(u<<1|1,l,r);
Node res;
pushup(res,left,right);
return res;
}
}
}
求Max和Sum
#include
#include
#include
using namespace std;
const int MAXN=510;
int n,m;
int w[MAXN];
struct Node{
int l,r;
int v;
int sum;
}tr[MAXN*4];
void pushup(int u){
tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r){
if(l==r) tr[u]={l,r,w[l],w[l]};
else{
int mid=l+r>>1;
tr[u]={l,r};
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
int modify(int u,int x,int v){
if(tr[u].l==x&&tr[u].r==x) tr[u].v=v;
else{
int mid=tr[u].l+tr[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
int queryMax(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].v;
else{
int mid=tr[u].l+tr[u].r>>1;
int v=0;
if(l<=mid) v = queryMax(u<<1,l,r);
else v=max(v,queryMax(u<<1|1,l,r));
return v;
}
}
int querySum(int u,int l,int r){
if(tr[u].l==l&&tr[u].r==r) return tr[u].sum;
else{
int mid=tr[u].l+tr[u].r>>1;
if(r<=mid) return querySum(u<<1,l,r);
else if(l>mid) return querySum(u<<1|1,l,r);
else return querySum(u<<1,l,mid)+querySum(u<<1|1,mid+1,r);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
build(1,1,n);
while(m--){
int p,x,y;
scanf("%d%d%d",&p,&x,&y);
if(p==1) modify(1,x,y);
else if(p==2) printf("Max=%dn",queryMax(1,x,y));
else printf("Sum=%dn",querySum(1,x,y));
}
return 0;
}
蓝桥杯 操作格子
https://buctcoder.com/problem.php?id=3178
#include
#include
#include
using namespace std;
typedef long long ll;
const int MAXN=100010;
int n,m;
int w[MAXN];
struct Node{
int l,r;
int v;
int sum;
}tr[MAXN*4];
void pushup(int u){
tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r){
if(l==r) tr[u]={l,r,w[l],w[l]};
else{
int mid=l+r>>1;
tr[u]={l,r};
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int x,int v){
if(tr[u].l==x&&tr[u].r==x) tr[u].v=v,tr[u].sum=v;
else{
int mid=tr[u].l+tr[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
int queryMax(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].v;
else{
int mid=tr[u].l+tr[u].r>>1;
if(r<=mid) return queryMax(u<<1,l,r);
else if(l>mid) return queryMax(u<<1|1,l,r);
else return max(queryMax(u<<1,l,mid),queryMax(u<<1|1,mid+1,r));
}
}
ll querySum(int u,int l,int r){
if(tr[u].l==l&&tr[u].r==r) return tr[u].sum;
else{
int mid=tr[u].l+tr[u].r>>1;
if(r<=mid) return querySum(u<<1,l,r);
else if(l>mid) return querySum(u<<1|1,l,r);
else return querySum(u<<1,l,mid)+querySum(u<<1|1,mid+1,r);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
build(1,1,n);
while(m--){
int p,x,y;
scanf("%d%d%d",&p,&x,&y);
if(p==1) modify(1,x,y);
else if(p==3) printf("%dn",queryMax(1,x,y));
else printf("%lldn",querySum(1,x,y));
}
return 0;
}
奇怪的宇宙
https://buctcoder.com/problem.php?cid=2614&pid=0
线段树DFS序列空间优化+__int128防爆
线段树结点k,左孩子为k+1,
若 right-left 是偶数,sizeof(左孩子) = right-left+1; (故右孩子为 k+right-left+2)
若 right-left 是奇数,sizeof(左孩子) = right-left; (故右孩子为 k+right-left+1)
#include
#include
#include
#include
#include
using namespace std;
const int MAXN=500010;
int w[MAXN];
struct Node{
int v;//最大值
int q;//最大值数量
}tr[MAXN*2-1];
void pushup(Node &u,Node &l,Node &r){
u.v=max(l.v,r.v);
if(l.v==r.v)
u.q=l.q+r.q;
else u.q=(l.v>r.v?l.q:r.q);
}
void pushup(int u,int lu,int ru){
pushup(tr[u],tr[lu],tr[ru]);
}
void build(int u,int l,int r){
if(l==r) tr[u]={w[l],1};
else{
int mid = l + r >> 1;
tr[u]={l,r};
int lu=u+1,ru=r-l&1?u+1+r-l:u+2+r-l;
build(lu,l,mid),build(ru,mid+1,r);
pushup(u,lu,ru);
}
}
Node query(int u,int l,int r,int cl,int cr){
if(l<=cl&&cr<=r) return tr[u];
else{
int mid=cl+cr>>1;
if(r<=mid) return query(u+1,l,r,cl,mid);
else if(l>mid) return query((cr-cl)&1?u+1+cr-cl:u+2+cr-cl,l,r,mid+1,cr);
else{
Node left=query(u+1,l,r,cl,mid);
Node right=query((cr-cl)&1?u+1+cr-cl:u+2+cr-cl,l,r,mid+1,cr);
Node res;
pushup(res,left,right);
return res;
}
}
}
void print(__int128 x){
stacks;
while(x){
s.push(x%10);
x /= 10;
}
while(!s.empty()){
printf("%d",s.top());
s.pop();
}
}
__int128 sum;
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
int l,r;
build(1,1,n);
while(m--){
scanf("%d%d",&l,&r);
if(l>r) swap(l,r);
Node t=query(1,l,r,1,n);
sum+=t.v*1ll*t.q;
}
print(sum);
return 0;
}
动态规划
背包问题
#动态规划 #dp
最长连续子串和
树形dp
状态压缩DP
数位DP
数位DP关注数的每一位上的数字。
数位DP用来解决一类特定类型的问题,他们有如下特征:
- 要求统计满足一定条件的数的数量;
- 这些条件经过转化后可以使用【数位】的思想去理解和判断;
- 输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
- 上界很大(比如
1
0
18
10^{18}
1018),暴力枚举验证会超时。
数位DP的基本原理:
考虑人类计数的方式,最朴素的计数就是从小到大开始依次加一。但我们发现对于位数比较多的数,这样的过程中有许多重复的部分。例如,从 7000 数到 7999、从 8000 数到 8999、和从 9000 数到 9999 的过程非常相似,它们都是后三位从 000 变到 999,不一样的地方只有千位这一位,所以我们可以把这些过程归并起来,将这些过程中产生的计数答案也都存在一个通用的数组里。此数组根据题目具体要求设置状态,用递推或 DP 的方式进行状态转移。
数位 DP 中通常会利用常规计数问题技巧,比如把一个区间内的答案拆成两部分相减,即
a
n
s
[
l
,
r
]
=
a
n
s
[
0
,
r
]
−
a
n
s
[
0
,
l
−
1
]
ans[l,r]=ans[0,r]-ans[0,l-1]
ans[l,r]=ans[0,r]−ans[0,l−1]
那么有了通用答案数组,接下来就是统计答案。统计答案可以选择记忆化搜索,也可以选择循环迭代递推。为了不重不漏地统计所有不超过上限的答案,要从高到低枚举每一位,再考虑每一位都可以填哪些数字,最后利用通用答案数组统计答案。
例题
✏️例1 Luogu P2602数字计数
题目大意:给定两个正整数a,b,求在[a,b]中的所有整数中,每个数码(digit)各出现了多少次。
方法一
对于i位数,他们组成的集合里所有数字出现的次数是相同的。
故设
d
p
i
dp_i
dpi为i位数的每个数字出现的次数(都是相同的就用一个表示),此时暂时不处理前导零。
则有
d
p
i
=
10
×
d
p
i
−
1
+
1
0
i
−
1
dp_i=10times dp_{i-1}+10^{i-1}
dpi=10×dpi−1+10i−1,这两部分前一个是来自前i-1位数字的贡献,后一个是来自第i位数字的贡献。
有了dp数组,我们来考虑如何统计答案。将上界按位分开,从高到低枚举,不贴着上界时,后面可以随便取值;贴着上界时,后面就只能取0到上界。分两部分分别计算贡献。最后考虑前导零,第i位为前导0时,此时1到i-1位也都是0,也就是多算了将i-1位填满的答案,需要额外减去。
#include
using namespace std;
const int N=15;
typedef long long ll;
//dp[i]表示i位数的每一位数字出现次数,sum[i]表示,mi[i]即幂,10^(i-1)。
ll l,r,dp[N],mi[N];
//ans1[N]记录右边界的每一位数字的情况,ans[2N]记录左边界的每一位数字的情况。
ll ans1[N],ans2[N];
//从低到高存储每一位
int a[N];
inline void solve(ll n,ll *ans){
ll tem=n;
//位数长度
int len=0;
//预处理a数组
while(n) a[++len]=n%10,n/=10;
//从高到低处理
for(int i=len;i>=1;--i){
//上界等于a[i]时,a[i]为1-9
for(int j=0;j<10;j++) ans[j]+=dp[i-1]*a[i];
//a[i]为0时的情况数
tmp-=mi[i-1]*a[i],ans[a[i]]+=tmp+1;
//上界小于a[i]时
for(int j=0;j
scanf("%lld%lld",&l,&r);
mi[0]=1ll;
//预处理dp数组,dp[i]=dp[i-1]*10+10^(i-1)
for(int i=1;i<=13;++i){
dp[i]=dp[i-1]*10+mi[i-1];
mi[i]=10ll*mi[i-1];
}
//计算ans1,ans2
solve(r,ans1),solve(l-1,ans2);
for(int i=0;i<10;++i) printf("%lld",ans1[i]-ans2[i]);
return 0;
}
度的数量
C
a
b
=
C
a
−
1
b
−
1
+
C
a
−
1
b
C_{a}^{b}=C_{a-1}{b-1}+C_{a-1}{b}
Cab=Ca−1b−1+Ca−1b
问题本质上是求解组合数
例如X=10,Y=1当K=2,B=2时,是求[1,10]之间的十进制数的二进制表示包含两位1的数量。
1
0
10
=
101
0
2
10_{10}=1010_{2}
1010=10102,
①从高到低遍历1010,第一位是1,此时last=0,res=0,我们可以从第2-3位中任意选(K-last)(2)位填1,
即res+=
C
3
2
C_{3}^{2}
C32,last++,
②第二位是0,此时res=3,last=1,跳过
③第三位是1,此时res=3,last=1,从第4位任意选(K-last)(1)位填1,即res+=
C
1
1
C_{1}^{1}
C11,last++,
④此时res=4,last=2,K-last=0,所有数字已遍历完,这是最后一种情况,也就是1010本身,故还需res++。
最终结果就是 res=5。
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f3f3f
const int N = 35;
int f[N][N];//f[i][j]表示从i个数里选j个数
int K,B;
void init(){
for(int i=0;i
if(!n) return 0;
vector nums;
while(n) nums.push_back(n%B),n/=B;
int res=0;//答案的数量
int last=0;//前面已经确定多少1
for(int i=nums.size()-1;i>=0;i--){//最高位往最低位枚举
int x=nums[i];
//求左边分支中的数的个数
if(x){
res+=f[i-1][K-last];//剩下i个位选K-last位,
if(x>1){
if(K-last-1>=0) res+=f[i][K-last-1];
break;
}
else{
last++;
if(last>K) break;
}
}
//最右侧分支的方案
if(!i&&last==K) res++;
}
return res;
}
int main(){
clock_t starttime,endtime;
starttime=clock();
init();
int l,r;
cin>>l>>r>>K>>B;
cout<
数字游戏
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f3f3f
const int N=15;
int f[N][N];//f[i][j]表示一共有i位 ,且最高位填j的数的个数
void init(){
for(int i=0;i<=9;i++) f[1][i]=1;
for(int i=2;i
if(!n) return 1;
vector nums;
while(n) nums.push_back(n%10),n/=10;
int res=0;
int last=0;
for(int i=nums.size()-1;i>=0;i--){
int x=nums[i];
for(int j=last;j
clock_t starttime,endtime;
starttime=clock();
int l,r;
init();
while(cin>>l>>r) cout<
Windy数
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f3f3f
const int N=11;
int f[N][10];
void init(){
for(int i=0;i<=9;i++) f[1][i]=1;
for(int i=2;i
for(int j=0;j<=9;j++)
for(int k=0;k<=9;k++)
if(abs(j-k)>=2)
f[i][j]+=f[i-1][k];
}
}
int dp(int n){
if(!n) return 0;
vector nums;
while(n) nums.push_back(n%10),n/=10;
int res=0;
int last=-2;
for(int i=nums.size()-1;i>=0;i--){
int x=nums[i];
for(int j=(i==nums.size()-1);j
if(abs(j-last)>=2)
res+=f[i+1][j];
}
if(abs(x-last)>=2) last=x;
else break;
if(!i) res++;
}
//特殊处理有前导0的数
for(int i=1;i
clock_t starttime,endtime;
starttime=clock();
init();
int l,r;
scanf("%d%d",&l,&r);
printf("%d",dp(r)-dp(l-1));
endtime=clock();
printf("n耗时:%f sn",(double)(endtime-starttime)/CLOCKS_PER_SEC);
return 0;
}
数字问题II
计算几何
STL
std::vector
定义于头文件
是封装动态数组的容器。
元素相继存储,这意味着不仅可通过迭代器,还能用指向元素的常规指针访问元素。
vector 的存储是自动管理的,按需扩张收缩。 vector 通常占用多于静态数组的空间,因为要分配更多内存以管理将来的增长。
vector 所用的方式不在每次插入元素时,而只在额外内存耗尽时重分配。分配的内存总量可用 capacity() 函数查询。额外内存可
通过对 shrink_to_fit() 的调用返回给系统。
元素访问:operator[]或迭代器。
成员函数
| begin() | 返回起始的迭代器 |
|---|
| end() | 返回指向末尾的迭代器 |
| rbebin() | 返回指向起始的逆向迭代器 |
| rend() | 返回指向末尾的逆向迭代器 |
| empty() | 检查容器是否为空 |
| size() | 返回容纳的元素数 |
| clear() | 清除内容 |
erase(iterator pos) erase(iterator first,iterator second) | 擦除位于pos元素 移除范围[first,last)中的元素 |
| push_back() | 将元素添加到末尾 |
| pop_back() | 移除末尾元素 |
std::basic_string
定义于头文件
为操作任何字符类型的字符串设计的模板类。
成员函数
1、find
寻找首个等于给定字符序列的子串。搜索始于 pos ,即找到的子串必须不始于 pos 之前的位置。
size_type find( const basic_string& str, size_type pos = 0 ) const;
查询首个等于str的字串
size_type find( CharT ch, size_type pos = 0 ) const;
查询首个等于ch的字符
没有查到返回npos
2、find_first_of
寻找字符的首次出现
3、find_last_of
寻找字符的最后一次出现
4、insert
插入字符
5、erase
移除字符
6、replace
替换字符串的指定位置
map
std::map 是有序键值对容器,它的元素的键是唯一的。用比较函数 Compare 排序键。搜索、移除和插入操作拥有对数复杂度。 map 通常实现为红黑树。
| 成员函数 | 作用 |
|---|
| size() | 返回容纳的元素数 |
| empty | 检查容器是否为空 |
| clear() | 清除内容 |
| insert | 插入元素或结点 |
erase size_type erase( const key_type& key ); | 擦除元素 移除关键等于 key 的元素(若存在一个)。 |
| count | 返回匹配特定键的元素数量 |
| find | 寻找带有特定键的元素 |
| contains | 检查容器是否含有带特定键的元素 |
| lower_bound | 返回指向首个不小于给定键的元素的迭代器 |
| upper_bound | 返回指向首个大于给定键的元素的迭代器 |
set
insert
#include
#include
#include
int main()
{
std::set set;
auto result_1 = set.insert(3);
assert(result_1.first != set.end()); // 它是合法迭代器
assert(*result_1.first == 3);
if (result_1.second)
std::cout << "insert donen";
auto result_2 = set.insert(3);
assert(result_2.first == result_1.first); // 相同迭代器
assert(*result_2.first == 3);
if (!result_2.second)
std::cout << "no insertionn";
}
queue
std::queue 类是容器适配器,它给予程序员队列的功能——尤其是 FIFO (先进先出)数据结构。queue 在底层容器尾端推入元素,从首端弹出元素。
练习题
最小权值
#include
#include
#include
#include
#include
#include
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f3f3f
#define maxn 2050
ll dp[maxn];//dp[i]表示有i个节点的树的权值。
ll dfs(int n){
if(dp[n]!=INF) return dp[n];
for(int i=0;i//左子树有0个结点到i-1个结点
dp[n]=min(dp[n],1+2*dfs(i)+3*dfs(n-i-1)+i*i*(n-i-1));
}
return dp[n];
}
int main(){
clock_t starttime,endtime;
starttime=clock();
for(int i=1;i<=2021;i++) dp[i]=INF;
ll ans=dfs(2021);
printf("%lld",ans);
endtime=clock();
printf("n耗时:%f sn",(double)(endtime-starttime)/CLOCKS_PER_SEC);
return 0;
}
123
关键是求得任意位置所在组号和在组内位置,
(
n
−
1
)
∗
n
2
<
x
≤
n
∗
(
n
+
1
)
2
n
=
r
o
u
n
d
(
2
∗
x
)
frac{(n-1)*n}{2}
pair getN(ll x){
ll n=round(sqrt(2*x));//四舍五入
return {n,x-(n-1)*n/2};//返回组号和组内位置或称之为值
}
异或变换
先模拟,找出循环节(循环多次后变为同样的二进制串)。
假设总共变换n次,而在变换t次后变为初始串,再经过t次也是同样结果,我们要避免剩下多余的运算,故再继续变换(n-t)/t次就够了。
PAT甲级 1018 测试点8,9
#include
#include
#include
#include
#include
using namespace std;
const int INF=99999999;
const int MAXN=510;
typedef long long ll;
int CMax,N,SP,M;
struct Node {
int u;
int dis;
int capacity;
bool operator>(const Node&a)const {
return dis>a.dis;
}
} node[MAXN];
struct Edge {
int v,w;
Edge(int v,int w):v(v),w(w){
}
};
vector Adj[MAXN];
bool vis[MAXN];
vector pre[MAXN];
int min_dis[MAXN];
priority_queue,greater > q;
int k=0;
void dijkstra() {
fill(min_dis,min_dis+MAXN,INF);
q.push(node[0]);
min_dis[0]=0;
while(!q.empty()) {
int u=q.top().u;
q.pop();
if(vis[u]) continue;
vis[u]=true;
for(vector::iterator i=Adj[u].begin();i!=Adj[u].end();i++) {
int v=i->v,w=i->w;
if(vis[v]) continue;
if(min_dis[v]>min_dis[u]+w) {
min_dis[v]=min_dis[u]+w;
node[v].dis=min_dis[v];
q.push(node[v]);
pre[v].clear();
pre[v].push_back(u);
}
else if(min_dis[v]==min_dis[u]+w) pre[v].push_back(u);
}
}
}
int minSent=INF,minBack=INF;
vector temppath,path;
void dfs(int v){
temppath.push_back(v);
if(v==0){
int sent=0,back=0;
for(int i=temppath.size()-1;i>=0;i--){
int id=temppath[i];
int t=node[id].capacity-(CMax>>1);
if(t>0){
back+=t;
}
else{
if(back>(0-t)) back+=t;
else sent+=((0-t)-back),back=0;
}
}
if(sent
minSent=sent;
minBack=back;
path=temppath;
}
temppath.pop_back();
return;
}
for(int i=0;i
cin>>CMax>>N>>SP>>M;
for(int i=1; i<=N; i++)
cin>>node[i].capacity,node[i].u=i;
for(int i=0; i
int x,y,w;
cin>>x>>y>>w;
Edge e= {y,w};
Adj[x].push_back(e);
}
dijkstra();
node[0].capacity=CMax>>1;
dfs(SP);
printf("%d 0", minSent);
for(int i = path.size() - 2; i >= 0; i--)
printf("->%d", path[i]);
printf(" %d", minBack);
return 0;
}