2024.2 总结
树
【CF771C】 Bear and Tree Jumps
题目描述
给出一个 \(n\) 个点的树,求 \(\sum^{}_{1 \le s \le t \le n} \frac{dis(s,t)}{k}\) ,向上取整,\(n \le 2 \times 10^5 , 1 \le k \le 5\) 。
解题思路
考虑枚举每个点对的 \(LCA\) ,计算对于每个 \(u\) 作为 \(LCA\) 的贡献有多少。
设 \(d_{i,j}\) 表示 \(i\) 的子树中到 \(i\) 的路径 \(\%k=j\) 的条数有多少,\(f_{i,j}\)表示其贡献。
很明显,这些东西可以东西可以通过 \(dp\) 转移。
已知这些东西后,我们可以考虑答案了。
我们可知:
把他们拆开,考虑 \(\frac{dis(u,s)}{k}\) 被加过多少次,同时 \(nk^2\) 暴枚 \(dis(u,s)\%k+dis(u,t)\%k>k\) 的数量即可。
Code
#include<bits/stdc++.h>
using namespace std;
long long n,m,f[200005][10],d[200005][10],s,siz[200005];
vector<long long> a[200005];
void dijah(long long x,long long y)
{
siz[x]=1,d[x][0]++;
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y)continue;
dijah(a[x][i],x);
for(int j=0;j<m;j++)
{
for(int u=1;u<m;u++)
{
if(j+u+1<=m)s-=d[a[x][i]][j]*d[x][u];
}
}
for(int j=0;j<m;j++)d[x][(j+1)%m]+=d[a[x][i]][j];
for(int j=0;j<m;j++)f[x][(j+1)%m]+=f[a[x][i]][j];
f[x][1%m]+=d[a[x][i]][0],siz[x]+=siz[a[x][i]];
}
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y)continue;
for(int j=0;j<m;j++)s+=(siz[x]-siz[a[x][i]])*(f[a[x][i]][j]+(j==0?d[a[x][i]][j]:0));
}
return;
}
int main()
{
long long x,y;
scanf("%lld%lld",&n,&m);
for(int i=1;i<n;i++)
{
scanf("%lld%lld",&x,&y);
a[x].push_back(y),a[y].push_back(x);
}
dijah(1,0);
cout<<s;
return 0;
}
【CF1009F】 Dominant Indices
题目描述
给定一棵以 \(1\) 为根,\(n\) 个节点的树。设 \(d(u,v)\) 表示 \(u\) 的子树中距离 \(u\) 为 \(v\) 的节点数。
对于每个点,求最小的 \(k\) 使得 \(d(u,k)\) 最大 ,\(n\le 10^6\) 。
解题思路
树上启发式合并板子题。
\(d(u,v)=deep_v-deep_u\) ,转化为求出现最多的深度,直接树上启发式合并,输出时减去该节点深度即可。
Code
#include<bits/stdc++.h>
using namespace std;
long long n,siz[1000005],deep[1000005],son[1000005],fa[1000005],f[1000005],dfn[1000005],out[1000005],num,re[1000005],d[1000005];
vector<long long> a[1000005];
void dfs1(long long x,long long y)
{
siz[x]=1,deep[x]=deep[y]+1,fa[x]=y;
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y)continue;
dfs1(a[x][i],x);
siz[x]+=siz[a[x][i]];
if(siz[a[x][i]]>siz[son[x]])son[x]=a[x][i];
}
return;
}
void dfs2(long long x,long long y)
{
dfn[x]=++num,re[num]=x;
if(son[x]!=0)dfs2(son[x],x);
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y||a[x][i]==son[x])continue;
dfs2(a[x][i],x);
}
out[x]=num;
return;
}
void dfs3(long long x,long long y)
{
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y||a[x][i]==son[x])continue;
dfs3(a[x][i],x);
}
if(son[x]!=0)
{
dfs3(son[x],x);
d[x]=d[son[x]],f[deep[x]]=1;
if(f[d[x]]==1)d[x]=deep[x];
for(int i=out[son[x]]+1;i<=out[x];i++)
{
f[deep[re[i]]]++;
if(f[deep[re[i]]]==f[d[x]])d[x]=min(d[x],deep[re[i]]);
if(f[deep[re[i]]]>f[d[x]])d[x]=deep[re[i]];
}
}
else f[deep[x]]++,d[x]=deep[x];
if(son[fa[x]]!=x)
{
for(int i=dfn[x];i<=out[x];i++)f[deep[re[i]]]--;
}
return;
}
int main()
{
long long x,y;
scanf("%lld",&n);
for(int i=1;i<n;i++)scanf("%lld%lld",&x,&y),a[x].push_back(y),a[y].push_back(x);
dfs1(1,0),dfs2(1,0),dfs3(1,0);
for(int i=1;i<=n;i++)printf("%lld\n",d[i]-deep[i]);
return 0;
}
【Luogu P2634】 聪聪可可
题目描述
给出一棵 \(n\) (\(n \le 2 \times 10^4\)) 个节点的树,边有边权,求有多少条路径的权值和为 \(3\) 的倍数。
解题思路
点分治模板题。
Code
#include<bits/stdc++.h>
using namespace std;
long long n,size[100005],minn[100005],root,maxx,all,f[3],f1[3],s;
bool v[100005];
vector<long long> a[100005],t[100005];
void search(long long x,long long y)
{
size[x]=1;
long long minn=0;
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y||v[a[x][i]])continue;
search(a[x][i],x);
size[x]+=size[a[x][i]],minn=max(minn,size[a[x][i]]);
}
minn=max(minn,all-size[x]);
if(minn<maxx)root=x,maxx=minn;
return;
}
void dfs(long long x,long long y,long long z)
{
f1[z%3]++;
for(int i=0;i<a[x].size();i++)
{
if(v[a[x][i]]||a[x][i]==y)continue;
dfs(a[x][i],x,z+t[x][i]);
}
return;
}
void gaia(long long x)
{
memset(f,0,sizeof(f));
f[0]=1;
for(int i=0;i<a[x].size();i++)
{
if(v[a[x][i]])continue;
memset(f1,0,sizeof(f));
dfs(a[x][i],0,t[x][i]);
for(int j=0;j<3;j++)s+=f[j]*f1[(3-j)%3];
for(int j=0;j<3;j++)f[j]+=f1[j];
}
return;
}
void dijah(long long x)
{
v[x]=true,gaia(x);
for(int i=0;i<a[x].size();i++)
{
if(v[a[x][i]])continue;
maxx=1e9+5,all=size[a[x][i]];
search(a[x][i],0),dijah(root);
}
return;
}
int main()
{
long long x,y,z;
scanf("%lld",&n);
for(int i=1;i<n;i++)
{
scanf("%lld%lld%lld",&x,&y,&z),z%=3;
a[x].push_back(y),a[y].push_back(x);
t[x].push_back(z),t[y].push_back(z);
}
maxx=1e9+5,all=n;
search(1,0),dijah(root);
s=s*2+n,z=__gcd(s,n*n);
printf("%lld/%lld",s/z,n*n/z);
return 0;
}
【Luogu P4886】 快递员
题目描述
给出一个 \(n\) 个点的树以及 \(m\) 个点对 \(x_i,y_i\),求 \(k\) 使得 \(max(dis(x_i,k)+dis(y_i,k))最小\) ,\(n,m \le 10^5\) 。
解题思路
首先,我们可以任意选择一个点作为 \(k\) ,然后判断是否能通过移动 \(k\) 使得答案更优。
容易发现,若最大贡献点对连成的路径经过了当前 \(k\) 节点,那么已经是最优的,可直接输出。
同时,将 \(k\) 作为根,若有多条最大贡献点对不经过 \(k\) 节点但在 \(k\) 的不同子树内,\(k\) 节点也已经是最优的。
否则,我们可以将 \(k\) 朝当前最大贡献点对所在的点对移动。
但不能一条边一条边的移动,这样会达到 \(O(nm)\) 的时间复杂度。
我们可以用点分治的思想,每次跳到重心上,这样的时间复杂度就为 \(O(mlogn)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long x,y,z;
}b[100005];
long long n,m,deep[100005],fa[100005],s,all,size[100005],root,qwe,s1=1e9+5;
vector<long long> a[100005],t[100005];
bool v[100005];
bool cmp1(datay q,datay w)
{
return q.z>w.z;
}
void dfs1(long long x,long long y)
{
fa[x]=fa[y];
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y)continue;
deep[a[x][i]]=deep[x]+t[x][i];
dfs1(a[x][i],x);
}
return;
}
void dfs2(long long x,long long y)
{
long long maxx=0;
size[x]=1;
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y||v[a[x][i]])continue;
dfs2(a[x][i],x);
size[x]+=size[a[x][i]],maxx=max(maxx,size[a[x][i]]);
}
maxx=max(maxx,all-size[x]);
if(maxx<qwe&&(!v[x]))qwe=maxx,root=x;
return;
}
void dijah(long long x)
{
memset(deep,0,sizeof(deep));
for(int i=0;i<a[x].size();i++)fa[x]=a[x][i],deep[a[x][i]]=t[x][i],dfs1(a[x][i],x);
for(int i=1;i<=m;i++)b[i].z=deep[b[i].x]+deep[b[i].y];
sort(b+1,b+m+1,cmp1);
s1=min(s1,b[1].z);
for(int i=1;i<=m;i++)
{
if(b[i].x==x)fa[x]=fa[b[i].y];
if(b[i].y==x)fa[x]=fa[b[i].x];
if(fa[b[i].x]!=fa[b[i].y])
{
s=b[i].z;
return;
}
if(b[i].z!=b[i+1].z)break;
if(fa[b[i].x]!=fa[b[i+1].x])
{
s=b[i].z;
return;
}
}
if(v[fa[b[1].x]])
{
s=b[1].z;
return;
}
v[x]=true,all=size[fa[b[1].x]],qwe=1e9+5;
dfs2(fa[b[1].x],x),dijah(root);
return;
}
int main()
{
long long x,y,z;
scanf("%lld%lld",&n,&m);
for(int i=1;i<n;i++)
{
scanf("%lld%lld%lld",&x,&y,&z);
a[x].push_back(y),a[y].push_back(x);
t[x].push_back(z),t[y].push_back(z);
}
for(int i=1;i<=m;i++)scanf("%lld%lld",&b[i].x,&b[i].y);
all=n,qwe=1e9+5;
dfs2(1,0),dijah(root);
printf("%lld",s1);
return 0;
}
图论
【Luogu P5590】 赛车游戏
题目描述
给你一个 \(n\) 点 \(m\) 边的有向图,输出一种给边定边权的方案,使得 \(1\) 到 \(n\) 的每条路径的权值和都相等,边权的范围为 \([1,9]\) ,\(n \le 1000,m \le 2000\) 。
解题思路
我们可以这样考虑:求出一条 \(1\) 到 \(n\) 的路径并定好边权,然后根据这条路径反推其他 \(1\) 到 \(n\) 的路径是否能合法即可。
设 \(1\) 到点 \(i\) 的距离为 \(d_i\) ,那么对于一条边 \((u,v)\),若让其合法,有 \(d_u+9 \le d_v\) 和 \(d_v-1 \le 9\) 。
这就是一个差分约束问题,用 \(SPFA\) 即可。
注意可以删去不在 \(1\) 到 \(n\) 的路径中的边。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long x,y;
}ll[10005];
long long n,m,f[1005];
bool v[1005],v1[1005],can;
vector<long long> b[1005],a[1005],t[1005];
queue<long long> l;
bool dfs1(long long x,long long y)
{
v1[x]=true;
for(int i=0;i<b[x].size();i++)
{
if(b[x][i]==y)continue;
if(!v1[b[x][i]])dfs1(b[x][i],x);
v[x]|=v[b[x][i]];
}
return v[x];
}
void dfs2(long long x,long long y)
{
v1[x]=true;
for(int i=0;i<b[x].size();i++)
{
if(b[x][i]==y||v[b[x][i]])continue;
if(v1[b[x][i]])can=true;
dfs2(b[x][i],x);
}
v1[x]=false,v[x]=true;
return;
}
void edge(long long x,long long y,long long z)
{
a[x].push_back(y),t[x].push_back(z);
return;
}
int main()
{
long long x,y;
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)scanf("%lld%lld",&ll[i].x,&ll[i].y),b[ll[i].x].push_back(ll[i].y);
memset(v,false,sizeof(v));
v[n]=true,dfs1(1,0);
if(can||(!v[1]))
{
printf("-1\n");
return 0;
}
for(int i=1;i<=n;i++)
{
if(v[i])
{
for(int j=0;j<b[i].size();j++)
{
if(v[b[i][j]])edge(i,b[i][j],9),edge(b[i][j],i,-1);
}
}
}
for(int i=1;i<=n;i++)v1[i]=v[i];
memset(v,false,sizeof(v)),memset(f,1,sizeof(f));
f[1]=0,v[1]=true,l.push(1);
for(int i=1;l.size();i++)
{
if(i>=2000000)
{
printf("-1\n");
return 0;
}
x=l.front(),l.pop(),v[x]=false;
for(int i=0;i<a[x].size();i++)
{
if(f[a[x][i]]>f[x]+t[x][i])
{
f[a[x][i]]=f[x]+t[x][i];
if(!v[a[x][i]])v[a[x][i]]=true,l.push(a[x][i]);
}
}
}
printf("%lld %lld\n",n,m);
for(int i=1;i<=m;i++)
{
if(v1[ll[i].x]&&v1[ll[i].y])printf("%lld %lld %lld\n",ll[i].x,ll[i].y,f[ll[i].y]-f[ll[i].x]);
else printf("%lld %lld 1\n",ll[i].x,ll[i].y);
}
return 0;
}
【Luogu P3039】 Delivery Route S
题目描述
给出 \(n\) 个点 \(x_i,y_i\) ,从第 \(1\) 个点出发,每次能上下左右走 \(1\) 距离,依次经过 \(2,3,...,n\) ,最后回到 \(1\) ,除了 \(1\) 点每个点只能经过一次,求走的最小距离,\(n \le 100,x_i,y_i \le 10^6\) 。
解题思路
很明显,从 \(i\) 走到 \(i+1\) 时不能经过除这 \(2\) 点以外的点。
直接暴力复杂度能到 \(10^9\) ,我们可以分析模拟一下题目走的过程。
可以发现,一个点到另一个点走的一定是直线,若路程中有点挡着,就应该从他旁边经过绕开。
所以,我们可以给每个点旁边的 \(4\) 个点建虚点,那么虚点之间的直线路径若无实点挡着,那么他们就是联通的,长度就是直线路径长度。
直接跑 \(Floyd\) 即可。
注意直线路径长度指的是曼哈顿距离。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long x,y,p;
}b[105],a[505];
long long n,num,dx[5]={0,1,-1,0,0},dy[5]={0,0,0,1,-1},f[505][505],d[505][505];
bool check1(long long x,long long q,long long w)
{
for(int i=1;i<=n;i++)
{
if(b[i].x==x&&b[i].y>=q&&b[i].y<=w)return true;
}
return false;
}
bool check2(long long y,long long q,long long w)
{
for(int i=1;i<=n;i++)
{
if(b[i].y==y&&b[i].x>=q&&b[i].x<=w)return true;
}
return false;
}
long long edge(long long qx,long long qy,long long wx,long long wy)
{
if(qx==wx)
{
if(qy>wy)swap(qy,wy);
return check1(qx,qy+1,wy-1)?(1e13+5):wy-qy;
}
if(qy==wy)
{
if(qx>wx)swap(qx,wx);
return check2(qy,qx+1,wx-1)?(1e13+5):wx-qx;
}
if(qx>wx)swap(qx,wx),swap(qy,wy);
if(qy>wy)return ((check1(qx,wy,qy-1)||check2(wy,qx,wx-1))&&(check2(qy,qx+1,wx)||check1(wx,wy+1,qy)))?(1e13+5):wx-qx+qy-wy;
return ((check2(qy,qx+1,wx)||check1(wx,qy,wy-1))&&(check1(qx,qy+1,wy)||check2(wy,qx,wx-1)))?(1e13+5):wx-qx+wy-qy;
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld%lld",&b[i].x,&b[i].y);
for(int i=1;i<=n;i++)
{
for(int u=1;u<=4;u++)
{
bool qwe=false;
for(int j=1;j<=n;j++)
{
if(b[i].x+dx[u]==b[j].x&&b[i].y+dy[u]==b[j].y)qwe=true;
}
if(!qwe)a[++num].x=b[i].x+dx[u],a[num].y=b[i].y+dy[u],a[num].p=i;
}
}
for(int i=1;i<=num;i++)
{
for(int j=1;j<=num;j++)
{
if(i==j)continue;
f[i][j]=edge(a[i].x,a[i].y,a[j].x,a[j].y);
}
}
for(int i=1;i<=num;i++)
{
for(int j=1;j<=num;j++)
{
for(int u=1;u<=num;u++)f[j][u]=min(f[j][u],f[j][i]+f[i][u]);
}
}
memset(d,1,sizeof(d));
long long s=0;
for(int i=1;i<=num;i++)
{
for(int j=1;j<=num;j++)d[a[i].p][a[j].p]=min(d[a[i].p][a[j].p],f[i][j]+2);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)d[i][j]=min(d[i][j],edge(b[i].x,b[i].y,b[j].x,b[j].y));
}
for(int i=1;i<n;i++)s+=d[i][i+1];
printf("%lld",s+d[n][1]>1e13?-1:s+d[n][1]);
return 0;
}
【Luogu P3965】 循环格
题目描述
给出一个 \(n \times m\) 的矩阵,每个点的值表示该点可以往上下左右某个方向走,每次可以修改一个点的值,求最小的修改次数,使得从任意位置出发都可以回到原位置,\(n,m\le 15\) 。
解题思路
图论中出现这种数据范围小的题,不是状压就是网络流,这题显然是网络流。
分析一下题目,发现若将连边,将会有 \(n \times m\) 的边,每个点的出度为 \(1\) 。
若有一点的入度 \(>1\) ,那么将会有一点的入度为 \(0\) ,不符合题目要求。
所以修改后的每个点的入度与出度都为 \(1\) 。
转化为一个二分图完美匹配问题,直接跑费用流即可。
Code
#include<bits/stdc++.h>
using namespace std;
struct edge
{
long long to,val,maxx,nex;
}a[100005];
long long num=-1,head[100005],start,endd,f[1005],n,d[1005],t[1005],val=0,now[1005],m;
queue<long long> l;
bool v[1005];
void add(long long x,long long to,long long maxx,long long val)
{
a[++num].to=to,a[num].val=val,a[num].maxx=maxx,a[num].nex=head[x];
head[x]=num;
return;
}
void edge(long long x,long long to,long long val,long long maxx)
{
add(x,to,maxx,val),add(to,x,0,-val);
return;
}
bool spfa()
{
memset(f,1,sizeof(f)),memset(v,false,sizeof(v)),memset(now,-1,sizeof(now));
while(l.size()!=0)l.pop();
l.push(start);
f[start]=0,v[start]=true,now[start]=head[start];
long long x,u,vv,maxx;
while(l.size())
{
x=l.front(),l.pop(),v[x]=false;
for(int i=head[x];i!=-1;i=a[i].nex)
{
vv=a[i].val,u=a[i].to,maxx=a[i].maxx;
if(maxx>0&&f[u]>f[x]+vv)
{
f[u]=f[x]+vv,now[u]=head[u];
if(!v[u])v[u]=true,l.push(u);
}
}
}
if(f[endd]>=1e9+5)return false;
return true;
}
long long dfs(long long x,long long y)
{
if(x==endd)
{
val+=y*f[endd];
return y;
}
v[x]=true;
long long u,vv,maxx,fl,now_y=y;
for(;now[x]!=-1;now[x]=a[now[x]].nex)
{
if(now_y==0)break;
u=a[now[x]].to,vv=a[now[x]].val,maxx=a[now[x]].maxx;
if(!v[u]&&maxx&&(f[u]==f[x]+vv))
{
fl=dfs(u,min(now_y,maxx));
now_y-=fl,a[now[x]].maxx-=fl,a[now[x]^1].maxx+=fl;
}
}
v[x]=false;
return y-now_y;
}
long long dinic()
{
long long qwe=0;
while(spfa())
{
memset(v,false,sizeof(v));
qwe+=f[endd]*dfs(start,1e9+5);
}
return qwe;
}
long long point(long long x,long long y)
{
if(x==0)x=n;
if(x==n+1)x=1;
if(y==0)y=m;
if(y==m+1)y=1;
return (x-1)*m+y;
}
int main()
{
string p;
memset(head,-1,sizeof(head));
scanf("%lld%lld",&n,&m);
start=2*n*m+1,endd=2*n*m+2;
for(int i=1;i<=n;i++)
{
cin>>p;
p=' '+p;
for(int j=1;j<p.size();j++)
{
if(p[j]=='U')edge(point(i,j),point(i-1,j)+n*m,0,1),edge(point(i,j),point(i+1,j)+n*m,1,1),edge(point(i,j),point(i,j+1)+n*m,1,1),edge(point(i,j),point(i,j-1)+n*m,1,1);
else if(p[j]=='D')edge(point(i,j),point(i-1,j)+n*m,1,1),edge(point(i,j),point(i+1,j)+n*m,0,1),edge(point(i,j),point(i,j+1)+n*m,1,1),edge(point(i,j),point(i,j-1)+n*m,1,1);
else if(p[j]=='R')edge(point(i,j),point(i-1,j)+n*m,1,1),edge(point(i,j),point(i+1,j)+n*m,1,1),edge(point(i,j),point(i,j+1)+n*m,0,1),edge(point(i,j),point(i,j-1)+n*m,1,1);
else if(p[j]=='L')edge(point(i,j),point(i-1,j)+n*m,1,1),edge(point(i,j),point(i+1,j)+n*m,1,1),edge(point(i,j),point(i,j+1)+n*m,1,1),edge(point(i,j),point(i,j-1)+n*m,0,1);
}
}
for(int i=1;i<=n*m;i++)edge(start,i,0,1),edge(i+n*m,endd,0,1);
cout<<dinic();
return 0;
}
【Luogu P8328】Usmjeravanje
题目描述
一个 \(2 \times n\) 个点的图,对于 \(1 \le i \le n,1 \le k \le n-i\) ,\(i\) 能到 \(i+k\) ,同时 \(i+n\) 能到 \(i+k+n\) ,同时有 \(m\) 条边 \(x_i,y_i\),满足 \(x_i \le n,y_i > n\) ,求一种给 \(m\) 条边定向的方案,使得图中的强联通分量的个数最多。(\(1 \le n,m \le 2\times 10^5\))
解题思路
模拟一下题目,发现有贡献的情况最终只有 \(3\) 种,他们都有共同点,最右下角的点都指向左上角。
所以我们可以据此确定边的方向,最后直接求强联通分量个数。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long x,y,p,v;
}b[200005];
long long n,m,k,dfn[400005],num,low[400005],s,v[400005];
vector<long long> a[400005];
stack<long long> l;
bool cmp1(datay q,datay w)
{
if(q.x!=w.x)return q.x<w.x;
return q.y>w.y;
}
bool cmp2(datay q,datay w)
{
return q.p<w.p;
}
void dijah(long long x)
{
v[x]=true,l.push(x),dfn[x]=low[x]=++num;
for(int i=0;i<a[x].size();i++)
{
if(!dfn[a[x][i]])dijah(a[x][i]),low[x]=min(low[x],low[a[x][i]]);
else if(v[a[x][i]])low[x]=min(low[x],dfn[a[x][i]]);
}
if(dfn[x]==low[x])
{
s++;
while(l.top()!=x)v[l.top()]=false,l.pop();
v[x]=false,l.pop();
}
return;
}
int main()
{
long long x=0;
scanf("%lld%lld%lld",&n,&m,&k);
for(int i=1;i<=k;i++)scanf("%lld%lld",&b[i].x,&b[i].y),b[i].p=i,b[i].y+=n;
sort(b+1,b+k+1,cmp1);
for(int i=1;i<=k;i++)
{
if(x<b[i].y)b[i].v=1,x=b[i].y,a[b[i].y].push_back(b[i].x);
else a[b[i].x].push_back(b[i].y);
}
for(int i=1;i<n;i++)a[i].push_back(i+1);
for(int i=1;i<m;i++)a[i+n].push_back(i+n+1);
for(int i=1;i<=n+m;i++)
{
if(!dfn[i])dijah(i);
}
sort(b+1,b+k+1,cmp2);
printf("%lld\n",s);
for(int i=1;i<=k;i++)printf("%lld ",b[i].v);
return 0;
}
数学/容斥/二项式反演
【Luogu P6576】Plus Minus
题目描述
给出一个 \(n \times m\) 的矩阵,每个点可以填 \(0\) 和 \(1\) ,同时有 \(k\) 条要求,每条要求为 \((x_i,y_i)\) 要为 \(v_i\) ,求有多少种可能满足每 \(2 \times 2\) 的矩阵内 \(0\) 和 \(1\) 的个数相同,\(n ,m \le 10^9,k \le 10^5\) 。
解题思路
若确定第一行与第一列的情况,那么我们可以确定出整个矩阵。
模拟一下,我们可以发现一个性质:第一行第一列中至少有一个是 \(01\) 交错的。
那么我们可以考虑容斥,答案为第一行 \(01\) 交错的答案 \(+\) 第一列 \(01\) 交错的答案 \(-\) 都 \(01\) 交错的答案。
第三种情况好做,以第一种情况为例讲一下第一二种情况如何求解。
若某一行没有限定,那么他有 \(2\) 种可能,否则,若能 \(01\) 交错,有 \(1\) 种可能,否则没有可能,直接乘起来即可。
注意特判 \(k=0\) 的情况。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
struct datay
{
long long x,y;
bool v;
}a[100005];
long long k,n,m,s1=1,s2=1,p,l1,l2;
long long dijah(long long x,long long y)
{
long long h=1;
while(y)
{
if(y&1)h=(h*x)%mod;
x=(x*x)%mod,y>>=1;
}
return h;
}
bool cmp1(datay q,datay w)
{
return q.x<w.x;
}
bool cmp2(datay q,datay w)
{
return q.y<w.y;
}
int main()
{
char c;
scanf("%lld%lld%lld",&n,&m,&k);
l1=n,l2=m;
for(int i=1;i<=k;i++)
{
cin>>c,scanf("%lld%lld",&a[i].x,&a[i].y);
if(c=='+')a[i].v=1;
else a[i].v=0;
}
sort(a+1,a+k+1,cmp1),p=1;
for(int i=1;i<=k;i++)
{
if(a[i].x!=a[i+1].x)
{
l1--;
for(int j=p;j<=i;j++)if(a[j].v==0)a[j].y++;
for(int j=p+1;j<=i;j++)if((a[j-1].y%2)!=(a[j].y%2))s1=0;
for(int j=p;j<=i;j++)if(a[j].v==0)a[j].y--;
p=i+1;
}
}
s1*=dijah(2,l1);
sort(a+1,a+k+1,cmp2),p=1;
for(int i=1;i<=k;i++)
{
if(a[i].y!=a[i+1].y)
{
l2--;
for(int j=p;j<=i;j++)if(a[j].v==0)a[j].x++;
for(int j=p+1;j<=i;j++)if((a[j-1].x%2)!=(a[j].x%2))s2=0;
p=i+1;
}
}
s2*=dijah(2,l2);
long long qwe=1;
for(int i=1;i<k;i++)if(((a[i].x+a[i].y)%2)!=((a[i+1].x+a[i+1].y)%2))qwe=0;
if(k==0)s1--;
printf("%lld",(s1+s2-qwe)%mod);
return 0;
}
【Luogu P4859】 已经没有什么好害怕的了
题目大意
给出长度为 \(n\) 的两个数组 \(a,b\) ,将 \(b\) 调换顺序,求不同 \(b\) 的个数满足 \(a_i>b_i\) 的恰好有 \(k\) 组,\(1 \le k\le n \le 2000\) 。
解题思路
看到恰好,我们可以考虑用二项式反演 \(+DP\) 来做。
将 \(a,b\) 排序,设 \(f_{i,j}\) 表示前 \(i\) 个中至少有 \(j\) 对已满足配对关系,那么转移方程为:
\(k\) 就为 \(b_{1...i}\) 中没被选过且小于 \(a_i\) 的个数,很明显 \(k=i-j+1\) 。
最后套二项式反演公式,用 \(f_{n,i}\) 计算即可。
Code
#include<bits/stdc++.h>
#define max(a,b) (a>b?a:b)
using namespace std;
const long long mod=1e9+9;
long long n,m,a[2005],b[2005],d[2005],f[2005][2005],C[2005][2005],s,p[2005];
int main()
{
long long r=1;
scanf("%lld%lld",&n,&m);
if((n+m)&1)
{
printf("0");
return 0;
}
m=(n+m)/2;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)scanf("%lld",&b[i]);
sort(a+1,a+n+1),sort(b+1,b+n+1);
for(int i=1;i<=n;i++)
{
while(b[r]<a[i]&&r<=n)r++;
d[i]=r-1;
}
f[0][0]=1;
for(int i=1;i<=n;i++)
{
f[i][0]=1;
for(int j=1;j<=i;j++)f[i][j]=(f[i-1][j]+max(d[i]-j+1,0)*f[i-1][j-1])%mod;
}
for(int i=0;i<=n;i++)C[i][0]=C[i][i]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
}
p[0]=1;
for(int i=1;i<=n;i++)p[i]=(p[i-1]*i)%mod;
for(int i=1;i<=n;i++)f[n][i]=(f[n][i]*p[n-i])%mod;
for(int i=m;i<=n;i++)s=(s+(((i-m)&1)?-1:1)*C[i][m]*f[n][i])%mod;
printf("%lld",(s+mod)%mod);
return 0;
}
【CF1228E】Another Filling the Grid
题目描述
给定一个 \(n \times n\) 的矩阵,用 \(1\)~\(k\) 的数填充,每行每列最小值均为\(1\),问有多少填法?( \(n\le 250,k \le 10^9\))
解题思路
套二项式反演,考虑如何求最多 \(i\) 行满足最小值为 \(1\) ,同时每列也满足最小值为 \(1\) 。
设 \(d_i\) 表示 \(i\) 个数中最小值为 \(1\) 的方案数,有 \(d_i=m^i-(m-1)^i\) 。
最多满足 \(i\) 行的答案即为 \(d_i \times (m-1)^{n^2-n}\times C(i,n)\) 。
最后套公式即可。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
long long n,m,d[255],C[255][255],s;
long long dijah(long long x,long long y)
{
long long h=1;
while(y)
{
if(y&1)h=(h*x)%mod;
x=(x*x)%mod,y>>=1;
}
return h;
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=0;i<=n;i++)C[i][0]=C[i][i]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
}
d[0]=1;
for(int i=1;i<=n;i++)d[i]=dijah(m,i)-dijah(m-1,i);
for(int i=1;i<=n;i++)s=(s+(((n-i)&1)?-1:1)*(dijah(d[i],n)*dijah(m-1,(n-i)*n)%mod)*C[n][i]%mod)%mod;
printf("%lld",(s+mod)%mod);
return 0;
}
【Luogu P6076】染色问题
题目描述
给出一个 \(n \times m\) 的矩阵,每个点的值可以为 \(0\) 到 \(k\) ,要求每行每列都要有一个位置不为 \(0\) ,同时 \(1\) 到 \(k\) 每个值都要出现过一次,求有多少种可能?(\(n,m,k \le 400\))
解题思路
设 \(f_i\) 表示至多出现了 \(i\) 种颜色的方案数,二项式反演,答案即为 \(\sum_{i=0}^{k} f_i\times C_{i}^{k}\) 。
\(f_i\) 的求法可以再用一次二项式反演。
设 \(d_i\) 为最多 \(i\) 行有填不为 \(0\) 的数,\(g_i\) 为 \(i\) 个数中有不为 \(0\) 的数的方案数,当前枚举的颜色数为 \(p\)。
\(g_i=(p+1)^i-1\),\(d_i=g_i^{m}\times C_{n}^{i}\),\(f_i=\sum_{i=0}^n d_i \times C_{n}^{i}\) 。
时间复杂度 \(O(knlogm)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
long long n,m,k,C[405][405];
long long dijah(long long x,long long y)
{
long long h=1;
while(y)
{
if(y&1)h=(h*x)%mod;
x=(x*x)%mod,y>>=1;
}
return h;
}
long long poi(long long l)
{
long long h=0;
for(int i=0;i<=n;i++)h=(h+(((n-i)&1)?-1:1)*C[n][i]%mod*dijah(dijah(l+1,i)-1,m)%mod)%mod;
return h;
}
int main()
{
long long s=0;
scanf("%lld%lld%lld",&n,&m,&k);
for(int i=0;i<=max(n,max(m,k));i++)C[i][0]=C[i][i]=1;
for(int i=1;i<=max(max(n,m),k);i++)
{
for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
}
for(int i=0;i<=k;i++)s=(s+(((k-i)&1)?-1:1)*C[k][i]*poi(i)%mod)%mod;
cout<<(s+mod)%mod;
return 0;
}
【CF997C】 Sky Full of Stars
题目描述
有一个 \(n \times n\) 的矩形,用红绿蓝三种颜色染色,求有多少种染色方案使得至少有同一行或同一列是同一种颜色,\(n \times 10^6\) 。
解题思路
设 \(f_{i,j}\) 为至少 \(i\) 行 \(j\) 列为同一种颜色,\(g_{i,j}\) 为恰好 \(i\) 行 \(j\) 列同一种颜色。
答案即为 \(m^{n\times n}-g_{0,0}\) 。
根据二项式反演公式, \(g_{x,y}=\sum_{i=x}^n \sum_{j=y}^n (-1)^{i+j-x-y} C_{n}^{i} C_{n}^{j} f_{i,j}\) 。
我们要求的是 \(g_{0,0}\) 有 \(0\) 的情况,需要分类讨论。
- \(i=0,j=0\)
这就是 \(m^{n \times n}\) 的情况了,所以答案转化为 \(\sum_{i=0}^n \sum_{j=0}^n (-1)^{i+j+1} C_{n}^{i} C_{n}^{j} f_{i,j}\) 。 - \(i=0\) 或 \(j=0\)
很好讨论,设 \(j=0\) ,即为 \(f_{i,0}=3^i 3^{n \times (n-i)}\) 。
\(i=0\) 的情况同理 - \(i,j\) 都不为 \(0\)
所有的贡献即为 \(\sum_{i=1}^n \sum_{j=1}^n (-1)^{i+j+1} C_{n}^i C_{n}^ j 3^{n\times n+i \times j -n \times i - n \times j + 1}\) 。
\(n\) 到了 \(10^6\) ,需要退一下式子。
考虑消掉一个 \(\sum\) ,原式 $= -3^{n\times n+1} \sum_{i=1}^n (-1)^i C_{n}^i 3^{-n \times i} \sum_{j=1}^{n} (-1)^j C_{n}^{j} 3^{j \times (i-n)} $ 。
后面的东西可以用二项式定理做 \(\sum_{j=1}^{n} (-1)^j C_{n}^{j} 3^{-n \times j + i \times j} = (1-3^{i-n})^n-1\) 。
那么就可以 \(O(nlogn)\) 求掉这些东西了。
把他们都加起来就是答案。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long n,p[1000005];
long long dijah(long long x,long long y)
{
y=(y%(mod-1)+mod-1)%(mod-1);
long long h=1;
while(y)
{
if(y&1)h=(h*x)%mod;
x=(x*x)%mod,y>>=1;
}
return h;
}
long long C(long long x,long long y)
{
return p[x]*dijah(p[x-y],mod-2)%mod*dijah(p[y],mod-2)%mod;
}
long long poi(long long x)
{
return dijah(1-dijah(3,x-n),n)-1;
}
int main()
{
scanf("%lld",&n);
p[0]=1;
for(int i=1;i<=n;i++)p[i]=(p[i-1]*i)%mod;
long long s=0,h=0;
for(int i=1;i<=n;i++)s=(s+((i&1)?-1:1)*C(n,i)*dijah(3,-i*n)%mod*poi(i))%mod;
s=((s*(dijah(3,n*n+1)%mod))%mod+mod)%mod;
for(int i=1;i<=n;i++)h=(h+((i&1)?-1:1)*C(n,i)*dijah(3,i)%mod*dijah(3,n*(n-i)))%mod;
s=((-s-2*h)%mod+mod)%mod;
printf("%lld",s);
return 0;
}
【Luogu P6478】 游戏
题目大意
给出一棵 \(n\) 个点的树,每个点有 \(01\) 两种权值,将 \(0\) 点与 \(1\) 点配对,求对于 \(i=0...m/2\) ,满足恰好有 \(i\) 组配对中 \(0\) 点或 \(1\) 点是另一个点的祖先的配对方案数。(\(n \le 5000\))
解题思路
看到恰好就想到二项式反演。
设 \(f_{i,j}\) 表示 \(i\) 节点中满足最多 \(j\) 组配对关系的方案数,这就是一个树形 \(dp\) 。
树上背包求出答案,然后套二项式反演公式即可。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long n,v[5005],f[5005][5005],size1[5005][2],pp[5005],rp[5005],d[5005];
vector<long long> a[5005];
long long poww(long long x,long long y)
{
long long h=1;
while(y)
{
if(y&1)h=(x*h)%mod;
x=(x*x)%mod,y>>=1;
}
return h;
}
void dijah(long long x,long long y)
{
long long p=0;
f[x][0]=1,size1[x][v[x]]++;
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y)continue;
dijah(a[x][i],x);
memset(d,0,sizeof(d));
for(int j=size1[x][0]+size1[x][1];j>=0;j--)
{
for(int u=size1[a[x][i]][0]+size1[a[x][i]][1];u>=0;u--)d[j+u]=(d[j+u]+f[x][j]*f[a[x][i]][u]%mod)%mod;
}
size1[x][0]+=size1[a[x][i]][0],size1[x][1]+=size1[a[x][i]][1];
for(int j=size1[x][0]+size1[x][1];j>=0;j--)f[x][j]=d[j];
}
for(int i=size1[x][v[x]^1]+1;i>=1;i--)f[x][i]=(f[x][i]+f[x][i-1]*(size1[x][v[x]^1]-i+1)%mod)%mod;
return;
}
long long C(long long x,long long y)
{
return (pp[x]*rp[x-y]%mod)*rp[y]%mod;
}
int main()
{
long long x,y;
string p;
scanf("%lld",&n);
pp[0]=1,rp[0]=1;
for(int i=1;i<=n;i++)pp[i]=(pp[i-1]*i)%mod;
for(int i=1;i<=n;i++)rp[i]=poww(pp[i],mod-2);
cin>>p;
for(int i=0;i<p.size();i++)v[i+1]=(p[i]=='1'?1:0);
for(int i=1;i<n;i++)scanf("%lld%lld",&x,&y),a[x].push_back(y),a[y].push_back(x);
dijah(1,0);
long long s=0;
for(int i=0;i<=n/2;i++)f[1][i]=(f[1][i]*pp[n/2-i])%mod;
for(int i=0;i<=(n/2);i++)
{
s=0;
for(int j=i;j<=n/2;j++)s=(s+(((j-i)&1)?-1:1)*C(j,i)*f[1][j]%mod)%mod;
printf("%lld\n",(s+mod)%mod);
}
return 0;
}
【Luogu P3935】 Calculating
题目描述
求 \(\sum_{i=l}^r d_(i)\) ,\(d_i\) 表示 \(i\) 的约数个数, \(1 \le l,r \le 10^{14}\) 。
解题思路
答案为 \(\sum_{i=1}^r d_{i}-\sum_{i=1}^{l-1} d_i\) ,考虑如何求 \(\sum_{i=1}^k d_i\) 。
每个因数 \(x\) 在 \(1\) 到 \(k\) 内出现了 \(k/x\) 次(向下取整),用整除分块即可。
时间复杂度 \(O(\sqrt r+\sqrt l)\)
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long gaia(long long n)
{
long long l=1,r,s=0;
for(;l<=n;l=r+1)r=(n/(n/l)),s=(s+(r-l+1)%mod*(n/l)%mod)%mod;
return s;
}
int main()
{
long long l,r;
scanf("%lld%lld",&l,&r);
printf("%lld\n",(gaia(r)-gaia(l-1)+mod)%mod);
return 0;
}
【CF1366D】 Two Divisors
题目描述
给定\(n\)整数\(a_1,a_2,\dots,a_n\),对于每个\(a_i\),求其两个约数\(d_1>1\)和\(d_2>1\),使得\(\gcd(d_1+d_2,a_i)=1\),或者说不存在这样的对。(\(n \le 5 \times 10^5 a_i \le 2 \times 10^7\))
解题思路
构造题。
一开始考虑直接用 \(a_i\) 两个不一样的约数,但这样做是错的, \(30\) 即是反例。
在原来基础上更改,设 \(a_i=p_1^{q_1} \times ... \times p_m^{q_m}\)考虑取两个数 \(x=p_z^{q_z},y=a_i/x,gcd(x,y)=1\) 。
考虑证明, \(gcd(x+y,a_i)=gcd(x+y,xy)=gcd(x+y,x)=gcd(x,y)=1\) 。
所以这样构造即可。
Code
#include<bits/stdc++.h>
using namespace std;
bool b[10000005];
long long n,a[500005],d1[500005],d2[500005],prime[10000005],l,f1[10000005],p;
map<long long,bool> g;
int main()
{
for(int i=2;i<=10000000;i++)
{
if(!b[i])
{
prime[++l]=i;
if(i<=3500)for(long long j=i;j<=10000000&&j>0;j*=i)g[j]=true;
}
for(int j=1;j<=l;j++)
{
if(i*prime[j]>10000000)break;
b[i*prime[j]]=1,f1[i*prime[j]]=prime[j];
if(i%prime[j]==0)break;
}
}
scanf("%lld",&n);
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
if(g.count(a[i]))
{
d1[i]=d2[i]=-1;
continue;
}
if(!f1[a[i]])
{
d1[i]=d2[i]=-1;
continue;
}
d1[i]=p=f1[a[i]];
while(p*f1[a[i]]>0&&a[i]%(p*f1[a[i]])==0)p*=f1[a[i]];
d2[i]=a[i]/p;
}
for(int i=1;i<=n;i++)printf("%d ",d1[i]);
printf("\n");
for(int i=1;i<=n;i++)printf("%d ",d2[i]);
return 0;
}
【Luogu P2303】 Longge 的问题
题目描述
求 \(\sum_{i=1}^n gcd(i,n)\) ,\(1 \le n \le 2^{32}\) 。
解题思路
暴力求 \(n\) 的因数即可。
Code
#include<bits/stdc++.h>
using namespace std;
long long n,a[100005],t[100005],l,s,qwe,p;
void dfs(long long x,long long y,long long z)
{
if(x==l+1)
{
s+=(qwe/y)*z;
return;
}
dfs(x+1,y,z),z*=(a[x]-1),y*=a[x];
for(int i=1;i<=t[x];i++)dfs(x+1,y,z),z*=a[x],y*=a[x];
return;
}
int main()
{
scanf("%lld",&n);
qwe=n,p=sqrt(n);
for(int i=2;i<=p;i++)
{
if(n%i==0)
{
a[++l]=i;
while(n%i==0)n/=i,t[l]++;
}
}
if(n!=1)a[++l]=n,t[l]=1;
dfs(1,1,1),cout<<s;
return 0;
}
【CF1139D】 Steps to One
题目描述
给一个数列,每次随机选一个 \(1\) 到 \(m\) 之间的数加在数列末尾,数列中所有数的 \(gcd=1\) 时停止,求期望长度。(\(1 \le m \le 10^5\))
解题思路
答案 \(ans=\sum_{i=1} P(len=i) \times i\) 。
\(len\) 恰好为多少很难做,考虑将其改成 \(len\) 至少为多少的形式。
一位一位的考虑,对于第 \(i\) 位,若 \(len \ge i\),则 \(+1\) ,所以有 \(ans=\sum_{i=1} P(len \ge i)\) 。
因为求一段序列的 \(gcd\) 恰好为 \(1\) 也不好求,考虑改成 \(gcd>1\) 的形式,\(ans=1+\sum_{i=1} P(len > i )\) 。
将概率拆开,\(ans=1+\sum_{i=1} \frac{\sum_{a_1=1}^m ... \sum_{a_i=1}^m gcd(a_1,...,a_m)>1}{m^i}\) 。
准备套莫反,\(gcd\) 的取值一定大于等于 \(1\) ,将 \(>1\) 反过来变成 \(=1\) ,\(ans=2-\sum_{i=1}\frac{\sum_{a_1=1}^m ... \sum_{a_i=1}^m gcd(a_1,...,a_m)=1}{m^i}=2- \sum_{i=1} \frac{\sum_{d=1}^m \mu(d) (\frac{m}{d})^i}{m^i}\) 。
提出 \(\sum\) ,\(ans=2-\sum_{d=1}^m \mu(d) \sum_{i=1} (\frac{m/d}{m})^i\) 。
用等比数列求和公式,\(ans=2-\sum_{d=1}^m \mu(d) \frac{m/d}{m-m/d}\) 。
由于 \(d=1\) 时不能用公式,提出来,所以 \(ans=1-\sum_{d=2}^m \mu(d) \frac{m/d}{m-m/d}\) 。
\(O(n)\) 筛出 \(\mu\) 即可做。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
long long n,s=0,prime[100005],l,mu[100005];
bool v[100005];
long long dijah(long long x,long long y)
{
long long h=1;
while(y)
{
if(y&1)h=(h*x)%mod;
x=(x*x)%mod,y>>=1;
}
return h;
}
int main()
{
scanf("%lld",&n);
mu[1]=1;
for(int i=2;i<=n;i++)
{
if(!v[i])prime[++l]=i,mu[i]=-1;
for(int j=1;j<=l;j++)
{
if(prime[j]*i>n)break;
v[i*prime[j]]=true;
if(i%prime[j]==0)
{
mu[i*prime[j]]=0;
break;
}
mu[i*prime[j]]=-mu[i];
}
}
for(int i=2;i<=n;i++)s=(s+mu[i]*(n/i)*dijah(n-n/i,mod-2))%mod;
printf("%lld",((-s+1)%mod+mod)%mod);
return 0;
}
【CF623B】 Array GCD
题目描述
给定长度为 \(n\) 的数组 \(a_i\),可以执行以下两个操作:
- 对于整个阵列,您可以删除长度为\(m(m<n)\)的间隔,代价是 \(m\times a\),该间隔只能操作一次。
- 对于每个元素,您可以花费\(b\)使其成为 \(+1\) 或 \(-1\),并且每个元素只能操作一次。
通过上述运算,求出使剩余数的最大公约数大于 \(1\) 的最小代价。(\(n \le 10^6,a_i \le 10^9\))
解题思路
一开始想到 \(dp\) 去了,后来发现是一道很奇妙的题。
因为不能删整个序列,改又只能改 \(1\) ,我们可以想到一个性质:\(a_1,a_n\) 中必有一个会留下,他们有可能会 \(+1/-1\) ,总共 \(6\) 中可能。
\(gcd\) 肯定是这些数的因数,必定是这些数的任意一个的质因数的约数。
那我们可以枚举出约数集合,然后考虑最小代价是多少即可。
对于枚举到 \(i\) ,设 \(i\) 为 \(gcd\) 的因数,因为不能全部删除,我们可以处理出 \(f_i\) 为只用 \(+1/-1\) 前 \(i\) 个使得 \(gcd\) 为 \(i\) 的倍数的最小代价,\(g_i\) 为后 \(i\) 个的最小代价。
我们考虑对于 \(dp_i\) 表示删除删到 \(i\) 的最小代价,分析一下,发现这个东西有一个很像一个决策单调性的东西,前面的代价统一加上 \(a\) ,只需与新加的决策比较即可。
这样做一次时间复杂度 \(O(n)\) ,总时间复杂度 \(O(nloga_i)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
long long n,m1,m2,a[1000005],d[1005],l,s=1e16+5,f[1000005],f1[1000005],f2[1000005];
bool check(long long x)
{
for(int i=1;i<=l;i++)if(d[i]==x)return false;
return true;
}
void add(long long x)
{
long long p=sqrt(x);
for(int i=2;i<=p;i++)
{
if(x%i==0)
{
if(check(i))d[++l]=i;
while(x%i==0)x/=i;
}
}
if(x!=1&&check(x))d[++l]=x;
return;
}
void gaia(long long x)
{
for(int i=1;i<=n;i++)
{
if(a[i]%x==0)f[i]=0;
else if(a[i]%x==1||a[i]%x==(x-1))f[i]=m2;
else f[i]=1e11+5;
}
for(int i=1;i<=n;i++)f1[i]=f1[i-1]+f[i];
for(int i=n;i>=1;i--)f2[i]=f2[i+1]+f[i];
long long p=1e16+5,s1=1e16+5;
for(int i=n;i>=1;i--)p=min(p,f2[i+1]),p+=m1,s1=min(s1,f1[i-1]+p);
s=min(s,min(s1,f1[n]));
return;
}
int main()
{
scanf("%lld%lld%lld",&n,&m1,&m2);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
add(a[1]+1),add(a[1]-1),add(a[1]);
add(a[n]+1),add(a[n]-1),add(a[n]);
sort(d+1,d+l+1);
for(int i=1;i<=l;i++)gaia(d[i]);
cout<<s;
return 0;
}
DP
【Luogu P9272】 千岛之国
题目描述
给出 \(n\) 个点及其坐标 \(x_i,y_i\) ,每个点 \(1\) 步可以跳到位于其左上或右下的点,求每个点要跳到其他所有点的最小步数的总和。(\(1 \le n \le 250000,1 \le x_i,y_i \le 2500\))
解题思路
看到 \(x_i,y_i\) 只有 \(2500\) ,考虑 \(dp\) 。
设 \(f_{i,j}\) 表示位于点 \((i,j)\) 走到位于其右上角的点的最小总步数,\(g_{i,j}\) 表示位于点 \((i,j)\) 走到位于其左下角的点的最小总步数。
以 \(f_{i,j}\) 为例,讲一下如何处理。
预处理出 \(f_{i,j}\) 能直接到的点的 \(x,y\) 的最值,然后我们每次右上推时我们可以得知哪些点还未被处理,直接跳过去即可。
实际上打记忆化搜索会更容易。
Code
#include<bits/stdc++.h>
using namespace std;
int n,a[2505][2505],cnt[2505][2505],lx[250005],ly[250005],a1[2505][2505],b1[2505][2505],a2[2505][2505],b2[2505][2505],f1[2505][2505],f2[2505][2505];
int gaia(int q,int w,int e,int r)
{
if(q>e||w>r)return 0;
return cnt[e][r]+cnt[q-1][w-1]-cnt[e][w-1]-cnt[q-1][r];
}
int gaia1(int x,int y)
{
if(gaia(1,y,x,2500)==0)f1[x][y]=0;
if(f1[x][y]!=-1)return f1[x][y];
f1[x][y]=gaia(1,y,x,2500)+gaia1(a1[x-1][y-1]>10000?x:a1[x-1][y-1],b2[x+1][y+1]==0?y:b2[x+1][y+1]);
return f1[x][y];
}
int gaia2(int x,int y)
{
if(gaia(x,1,2500,y)==0)f2[x][y]=0;
if(f2[x][y]!=-1)return f2[x][y];
f2[x][y]=gaia(x,1,2500,y)+gaia2(a2[x+1][y+1]==0?x:a2[x+1][y+1],b1[x-1][y-1]>10000?y:b1[x-1][y-1]);
return f2[x][y];
}
int main()
{
int x,y;
scanf("%d",&n);
if(n==1)
{
printf("0\n");
return 0;
}
for(int i=1;i<=n;i++)scanf("%d%d",&lx[i],&ly[i]),a[lx[i]][ly[i]]=1;
memset(a1,1,sizeof(a1)),memset(b1,1,sizeof(b1));
for(int i=1;i<=2500;i++)
{
for(int j=1;j<=2500;j++)
{
cnt[i][j]=a[i][j]+cnt[i-1][j]-cnt[i-1][j-1]+cnt[i][j-1];
if(a[i][j])a1[i][j]=min(i,a1[i-1][j]),b1[i][j]=min(j,b1[i][j-1]);
else a1[i][j]=min(a1[i-1][j],a1[i][j-1]),b1[i][j]=min(b1[i-1][j],b1[i][j-1]);
}
}
for(int i=2500;i>=1;i--)
{
for(int j=2500;j>=1;j--)
{
if(a[i][j])a2[i][j]=max(i,a2[i+1][j]),b2[i][j]=max(j,b2[i][j+1]);
else a2[i][j]=max(a2[i+1][j],a2[i][j+1]),b2[i][j]=max(b2[i+1][j],b2[i][j+1]);
}
}
memset(f1,-1,sizeof(f1)),memset(f2,-1,sizeof(f2));
for(int i=1;i<=n;i++)printf("%d\n",cnt[2500][2500]+gaia1(lx[i],ly[i])+gaia2(lx[i],ly[i])-3);
return 0;
}
【CF1242C】Sum Balance
题目描述
有 \(k\) 个盒子,第 \(i\) 个盒子有 \(n_i\) 个数,每个数不相同。从每个盒子中取出一个数再放到一个盒子中,使得最后每个盒子里面的数的和相同,求任意方案。(\(1 \le k \le 15,1\le n \le 5000\))
解题思路
我们可以求出最后每个盒子里面的数的和是多少,这样的话,我们就可以知道若取出一个数,应该放一个什么数进来。
由于每个数都不相同,所以每个点指向的节点唯一。
变成了一个内向基环树找环的问题,每次找出环后状压所在的盒子编号,然后跑状压 \(dp\) 即可。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long v,p,to;
}b[100005],t[25];
vector<long long> a[100005];
long long n,num,d[25],f[100005],l[100005],r,can=0,qw=0,ll[100005];
bool v1[100005],v[100005];
bool cmp(datay q,datay w)
{
return q.v<w.v;
}
bool cmp1(datay q,datay w)
{
return q.p<w.p;
}
void dijah(long long x)
{
if(v1[x])return;
if(v[x])
{
memset(d,0,sizeof(d));
for(int i=r;i>=1;i--)
{
if(l[i]==x)break;
if(d[b[l[i]].p])return;
d[b[l[i]].p]=1;
}
can+=(1<<(b[x].p-1));
for(int i=r;i>=1;i--)
{
if(l[i]==x)return;
can+=(1<<(b[l[i]].p-1));
}
return;
}
v[x]=true;
for(int i=0;i<a[x].size();i++)
{
l[++r]=x,dijah(a[x][i]),r--;
if(can)break;
}
v[x]=false,v1[x]=true;
return;
}
void check(long long x)
{
qw=0;
while(!v[x])
{
ll[++qw]=x,v[x]=true,x=a[x][0];
}
ll[++qw]=x;
for(int i=qw;i>=1;i--)
{
if(i!=qw&&ll[i]==x)return;
t[++r]=b[ll[i]],t[r].to=b[ll[i-1]].p;
}
return;
}
void dfs(long long x)
{
if(x==0)return;
check(f[x^l[x]]),dfs(l[x]);
return;
}
int main()
{
long long x,y,p=0,ll,rr,mid;
scanf("%lld",&n);
for(int i=1;i<=n;i++)
{
scanf("%lld",&x);
for(int j=1;j<=x;j++)b[++num].p=i,scanf("%lld",&b[num].v),p+=b[num].v,d[i]+=b[num].v;
}
sort(b+1,b+num+1,cmp),p/=n;
for(int i=1;i<=num;i++)
{
x=p-d[b[i].p]+b[i].v,y=0,ll=1,rr=num;
while(ll<=rr)
{
mid=(ll+rr)>>1;
if(b[mid].v<=x)y=max(y,mid),ll=mid+1;
else rr=mid-1;
}
if(b[y].v==x)a[i].push_back(y);
}
for(int i=1;i<=num;i++)can=0,r=0,dijah(i),f[can]=i;
memset(v,false,sizeof(v)),memset(l,-1,sizeof(l));
p=(1<<n),v[0]=true;
for(int i=1;i<p;i++)
{
for(int j=i;j>0;j=(j-1)&i)
{
if(f[j]&&v[i^j])
{
v[i]=true,l[i]=i^j;
break;
}
}
}
if(l[p-1]==-1)
{
printf("No");
return 0;
}
memset(v,false,sizeof(v)),memset(v1,false,sizeof(v1));
r=0,dfs(p-1);
sort(t+1,t+r+1,cmp1);
printf("Yes\n");
for(int i=1;i<=r;i++)printf("%lld %lld\n",t[i].v,t[i].to);
return 0;
}
【CF1295F】 Good Contest
题目描述
\(a_i\) 在 \([l_i,r_i]\) 上均匀随机分布,求 \(a_1...n\) 单调不增的概率,\(1 \le n \le 50\)。
解题思路
个人认为将区间反过来,转化成单调不减的概率会好处理一些。
求概率就是可行方案数除以总方案数,问题变成求有多少可行方案。
\(l_i,r_i\) 都很大,要将区间离散化成一个个左闭右开的区间。
我们就可以以区间为单位进行 \(dp\) 。
由于以区间内每个数结尾的答案不一样,是一个高次函数的形式,所以我们要一整个区间的考虑。
设 \(f_{i,j}\) 表示处理到 \(a_i\) ,以第 \(j\) 个区间里面的数为结尾的方案数。
转移考虑在 \(i\) 前面有多少个数选择在区间 \(j\) ,即:
枚举开始转移的第 \(x\) 位,转移前的最后一位区间为第 \(y\) 个区间,\(len_j\) 表示区间 \(j\) 的长度,\(p(x,y)\) 表示 \(x\) 个数每个数都在 \(y\) 个数里面取,求单调不减的方案数。
考虑 \(p(x,y)\) 如何处理,我们可以得到一条转移式:\(p(x,y)=p(x-1,y)+p(x-1,y-1)\) 。
如果 \(x,y\) 不大,那么可以预处理,但这里不行,我们再观察发现,这是杨辉三角的转移式,说明这是一个组合数,模拟一下,我们发现:\(p(x,y)=C_{x+y}^{x}\) 。
其实这个东西还有一个更简单的思考方法:构造一个从 \(1\) 到 \(y\) 从小到大的序列,在里面选 \(x\) 个,即为 \(x\) 个数在 \([1,y]\) 里面取单调上升的方案数,但它是能相等的,所以我们在序列前面加 \(x\) 个 \(0\) ,每次选到第 \(i\) 个 \(0\) 就表示第 \(i\) 位与上一位填的数相同,那么就是 \(C_{x+y}^x\) 。
同时,原 \(dp\) 式可以用前缀和进一步优化,即:
其中 \(d_{i,j}=\sum_{x=1}^{j} f_{i,x}\) 。
时间复杂度 \(O(n^3)\) ,可过 \(n=200\) 的数据 。
注意组合数要在 \(dp\) 的时候同步处理,每一次在上一次的基础上改就行了。
Code
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
struct datay
{
long long x,y;
}a[505];
long long n,dd[1005],t[100005],f[505][1005],d[505][1005],qwe=1;
set<long long> l1;
map<long long,long long> p;
long long dijah(long long x,long long y)
{
long long h=1;
while(y)
{
if(y&1)h=(h*x)%mod;
x=(x*x)%mod,y>>=1;
}
return h;
}
int main()
{
long long x,y,z;
scanf("%lld",&n);
for(int i=1;i<=n;i++)
{
scanf("%lld%lld",&a[i].x,&a[i].y);
qwe=qwe*(a[i].y-a[i].x+1)%mod,a[i].x=998244352-a[i].x,a[i].y=998244352-a[i].y,swap(a[i].x,a[i].y),a[i].y++;
l1.insert(a[i].x),l1.insert(a[i].y);
}
set<long long>::iterator q=l1.begin();
long long g=0;
for(;q!=l1.end();q++)p[*q]=++g,dd[g]=*q;
for(int i=1;i<=n;i++)a[i].x=p[a[i].x],a[i].y=p[a[i].y];
for(int i=1;i<=n;i++)t[i]=dijah(i,mod-2);
for(int i=0;i<=2*n;i++)d[0][i]=1;
for(int i=1;i<=n;i++)
{
for(int j=a[i].x;j<a[i].y;j++)
{
x=dd[j+1]-dd[j],y=1,z=0;
for(int u=i-1;u>=0;u--)
{
if(a[u+1].x<=j&&a[u+1].y>j)z++,y=(y*t[z]%mod)*(x+z-1)%mod;
else break;
f[i][j]=(f[i][j]+y*d[u][j-1])%mod;
}
}
for(int j=1;j<=2*n;j++)d[i][j]=(d[i][j-1]+f[i][j])%mod;
}
printf("%lld",d[n][2*n]*dijah(qwe,mod-2)%mod);
return 0;
}
【Luogu P4158】 粉刷匠
题目描述
有 \(n\) 条木板,每条木板有 \(m\) 个格子,每个格子有 \(0/1\) 权值,现在有 \(T\) 次操作,每次操作可以选定以个权值并选一条木板,选定木板上的一段的区间,贡献加上这段区间里面的与选定权值相同的权值的数量,每个格子只能被选一次,求最大贡献?(\(n,m \le 50,T \le 2500\))
解题思路
木板之间互不影响,我们可以对每条木板都做一次 \(dp\) ,计算在这条木板用 \(i\) 次操作所得的最大贡献,这一步的时间复杂度是 \(O(nmT)\) 。
同时,再做一次 \(dp\) 计算前 \(i\) 块木板用 \(j\) 的代价所得的最大贡献,时间复杂度 \(O(nT^2)\) 。
卡一下就能过了。
Code
#include<bits/stdc++.h>
#define max(a,b) (a>b?a:b)
using namespace std;
int n,m,k,a[55][2505],f[55][55][2505],d[55][2505];
int main()
{
int q,w;
string p;
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)
{
cin>>p;
for(int j=0;j<p.size();j++)a[i][j+1]=a[i][j]+p[j]-'0';
}
if(k>=n*m)
{
printf("%d\n",n*m);
return 0;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
for(int u=0;u<j;u++)
{
for(int q=1;q<=m;q++)f[i][j][q]=max(f[i][j][q],f[i][u][q-1]+max(a[i][j]-a[i][u],j-u-a[i][j]+a[i][u]));
}
}
}
for(int i=1;i<=n;i++)
{
q=min(i*m,k);
for(int j=1;j<=q;j++)
{
for(int u=0;u<j;u++)d[i][j]=max(d[i][j],d[i-1][u]+f[i][m][j-u]);
}
}
printf("%d",d[n][k]);
return 0;
}
【CF868F】 Yet Another Minimization Problem
题目描述
给出一个长度为 \(n\) 的序列 \(a\) ,将序列划分成 \(k\) 段,每段的代价为里面相同元素的对数,求最小总代价,\(1 \le n \le 10^5,1 \le k \le 20\) 。
解题思路
能很快列出一个 \(O(n^2k)\) 的 \(dp\) ,\(f_{i,j}=max_{x=1}^{n-1} f_{x,j-1}+p(x+1,i)\) ,\(p(x,y)\) 为子段 \([x,y]\) 的代价。
考虑优化,对于 \(f_{i,j}\) 设它的最优决策点为 \(k\) ,对于 \(f_{i+1,j}\) ,每一位的 \(p(x,i)\) 都会增加,很明显对于决策点 \(k_1<k\) ,它明显还是不如 \(k\) 优的,所以这具有决策单调性。
解决决策单调性有二分栈/队列、分治等的方法, 第一个方法需要快速求出 \(p(x,y)\) ,这题显然不行,我们考虑分治。
将 \(dp\) 的顺序调换,先考虑分了多少段,再考虑做到第几位。
分治,设做到的需要求决策点的区间是 \([l,r]\) ,它的决策点在 \([L,R]\) ,我们可以取 \(mid=(l+r)/2\) ,求出 \(i=mid\) 时的决策点 \(k\),然后递归,\([l,mid-1]\) 的区间的决策点范围在 \([L,k]\) ,\([mid+1,r]\) 的决策点的范围在 \([k,R]\) 。
如何快速计算 \(p(x,y)\) ,我们可以运用类似莫队的的思想,在分治的过程中 \(p(x,y)\) 左右端点最多移动 \(O(nlogn)\) 次,直接移动即可。
做 \(k\) 次分治,时间复杂度 $O(knlogn) $
Code
#include<bits/stdc++.h>
using namespace std;
long long n,m,a[100005],k,f[25][100005],l=1,r=0,s=0,d[100005];
void add(long long x)
{
s+=d[x],d[x]++;
return;
}
void del(long long x)
{
d[x]--,s-=d[x];
return;
}
long long dijah(long long x,long long y)
{
while(l<x)del(a[l]),l++;
while(l>x)l--,add(a[l]);
while(r<y)r++,add(a[r]);
while(r>y)del(a[r]),r--;
return s;
}
void gaia(long long l,long long r,long long L,long long R)
{
long long mid=(l+r)>>1,p;
for(int i=L;i<=R;i++)
{
if(f[k][mid]>f[k-1][i-1]+dijah(i,mid))p=i,f[k][mid]=f[k-1][i-1]+dijah(i,mid);
}
if(l==r)return;
gaia(l,mid,L,p),gaia(mid+1,r,p,R);
return;
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
memset(f,1,sizeof(f));
f[0][0]=0;
for(int i=1;i<=m;i++)k=i,gaia(1,n,1,n);
cout<<f[m][n];
return 0;
}
【CF494C】 Helping People
题目描述
有一个长度为 \(n\) 的序列,初始时为 \(a_{1...n}\) ,给 \(m\) 个操作,第 \(i\) 个操作有 \(p_i\) 的概率让 \([l_i,r_i]\) 里面的数都 \(+1\) ,保证区间不会相交,即为没有重合部分或全包含,求最大值期望。(\(1 \le n \le 10^5,1 \le m \le 5000\))
解题思路
保证了区间不会相交,我们可以把这个问题转成一树上问题。
每个区间的父亲为包含它的最小那个区间,再建一个 \([1,n],p=0\) 的区间,将森林变成一棵树。
考虑树形 \(dp\) ,直接做可能有些困难,考虑如何设计。
只有加后的值高过原区间 \(max\) 才会有价值,我们可以据此离散化一下,同时,处理最小值会比处理特定值容易一些,所以我们可以用 \(ST\) 表处理一个区间的最大值,\(dp\) 时就设 \(f_{i,j}\) 第 \(i\) 个区间,最大值比原区间最大值最多多 \(j\) 的概率,这样就可以很快从儿子转移。
最后 \(ans=\sum_{i=0}^{m} (max_{[1,n]}+i) \times (f_{root,i}-f_{root,i-1})\) 。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long x,y,z;
double p;
}b[5005],d[100005];
long long n,m,v1[100005],fa[5005],r,maxx,f1[100005][21],l[100005],p1[21];
double f[5005][5005],s;
vector<long long> a[5005];
bool cmp1(datay q,datay w)
{
if(q.x!=w.x)return q.x<w.x;
return q.y>w.y;
}
bool cmp2(datay q,datay w)
{
return q.x>w.x;
}
void dijah(long long x,long long y)
{
f[x][0]=(1-b[x].p);
for(int i=0;i<a[x].size();i++)
{
if(a[x][i]==y)continue;
dijah(a[x][i],x);
f[x][0]*=f[a[x][i]][b[x].z-b[a[x][i]].z];
}
double q=1,w=1;
for(int i=1;i<=m;i++)
{
q=w=1;
for(int j=0;j<a[x].size();j++)
{
if(a[x][j]==y)continue;
q*=f[a[x][j]][min(i+b[x].z-b[a[x][j]].z-1,m)],w*=f[a[x][j]][min(i+b[x].z-b[a[x][j]].z,m)];
}
f[x][i]+=b[x].p*q+(1-b[x].p)*w;
}
return;
}
long long gaia(long long x,long long y)
{
long long z=l[y-x+1];
return max(f1[x][z],f1[y-p1[z]+1][z]);
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)scanf("%lld",&v1[i]),f1[i][0]=v1[i];
l[0]=-1,p1[0]=1;
for(int i=1;i<=20;i++)p1[i]=p1[i-1]*2;
for(int i=1;i<=n;i++)l[i]=l[i>>1]+1;
for(int i=1;p1[i]<=n;i++)
{
for(int j=1;j+p1[i]-1<=n;j++)f1[j][i]=max(f1[j][i-1],f1[j+p1[i-1]][i-1]);
}
maxx=gaia(1,n);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld",&b[i].x,&b[i].y);
cin>>b[i].p;
}
long long x;
sort(b+1,b+m+1,cmp1);
b[0].x=1,b[0].y=n,b[0].z=maxx;
for(int i=1;i<=m;i++)
{
b[i].z=gaia(b[i].x,b[i].y),x=i-1;
while(x!=0&&b[x].y<b[i].y)x=fa[x];
fa[i]=x,a[i].push_back(x),a[x].push_back(i);
}
f[0][0]=1,dijah(0,0);
double s=0;
for(int i=m-1;i>=0;i--)s+=(f[0][i+1]-f[0][i])*(i+maxx+1);
s+=f[0][0]*(maxx),printf("%.9lf",s);
return 0;
}
【Luogu P1973】 NOI 嘉年华
题目描述
给出 \(n\) 个区间 \(l_i,r_i\) ,将每个区间分到两堆中或者不分,使得最后的一堆内的区间与另一堆内的每个区间都不相交,分完后的价值为分到的区间较少的那堆的区间数,求最大价值,以及钦定必须选某个区间后最大价值。(\(1 \le n \le 200\))
解题思路
考虑 \(dp\) 。
我们可以先设 \(p(i,j)\) 表示 \([i,j]\) 内有多少个区间,这个可以 $O(n^3) $ 暴力求。
让最小值最大不好用 \(dp\) 处理,一般用二分,这里我们设 \(f_{i,j}\) 表示处理到第 \(i\) 个区间,往第一堆里面放了 \(j\) 个区间,第二堆放的最大区间数量。
转移就很简单了,\(dp\) 可以 \(O(n^3)\) 做完,这样我们就解决了未钦定的问题。
倒着再来一遍上面的 \(dp\) 设为 \(g_{i,j}\) 表示最后 \(i\) 个往第一堆里面放 \(j\) 个区间第二堆的最大区间数,设钦定一定要分第 \(x\) 个区间,我们可以将这个区间直接加到一个堆中,这里假设是第二堆,有一种错误解法是直接输出 \(max_{i=1}^n max_{j=1}^n min(i+j+1,f_{l_x-1,i}+g_{r_x+1,j})\) 。
上面的解法错的原因是因为钦定的区间可以和第二堆中的其他区间相交,所以我们还要枚举第一堆的结束位置与再次开始的位置,这样一次的时间复杂度为 \(O(n^4)\) ,总就是 \(O(n^5)\) 的。
写出式子:\(max_{i_1=1}^{l_x-1} max_{i_2=1}^{r_x+1} max_{j_1=1}^{n} max_{j_2=1}^{n} min(j_1+j_2+p(i_1+1,i_2-1),f_{i_1,j_1}+g_{i_2,j_2})\) ,发现后面的两个 \(max\) 与 \(l_x,r_x\) 是无关的,这样我们就可以预处理。
设 \(t(l,r)=max_{i=1}^n max_{j=1}^n min(i+j+p(l+1,r-1),f_{l,i}+g_{r,j})\) ,这样每次解决询问的时间复杂度的时间复杂度是 \(O(n^2)\) 的,总就是 \(O(n^3)\) 。
但处理这东西的时间复杂度是 \(O(n^4)\) ,我们可以考虑决策单调性之类的优化手段。
因为 \(min(a,b)\) 里面的 \(a,b\) 是可以互相转换的,我们强行钦定 \(a>b\) 没有影响,所以我们令 \(f_{l_i}+g_{r,j}<i+j+p(l+1,r-1)\) ,那么很明显,随着 \(i\) 的增加,\(j\) 减少才能使答案更优。
所以我们枚举 \(i\) ,不断减 \(j\) 直到不行为止,这样的时间复杂度为 \(O(n^3)\) 的。
总时间复杂度就为 \(O(n^3)\)
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long x,y;
}a[205];
long long n,m,b[405][405],f1[405][405],f2[405][405],d[405][405],f[405][405];
set<long long> l;
map<long long,long long> p;
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld%lld",&a[i].x,&a[i].y),l.insert(a[i].x),l.insert(a[i].y+=a[i].x-1);
set<long long>::iterator q=l.begin();
for(;q!=l.end();q++)p[*q]=++m;
for(int i=1;i<=n;i++)a[i].x=p[a[i].x],a[i].y=p[a[i].y];
for(int i=1;i<=n;i++)
{
for(int j=1;j<=a[i].x;j++)
{
for(int u=a[i].y;u<=m;u++)b[j][u]++;
}
}
memset(f1,-2,sizeof(f1)),memset(f2,-2,sizeof(f2));
f1[0][0]=0,f2[m+1][0]=0;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=i;j++)
{
for(int u=0;u<b[j][i];u++)f1[i][u]=max(f1[i][u],f1[j-1][u]+b[j][i]);
for(int u=b[j][i];u<=n;u++)f1[i][u]=max(f1[i][u],max(f1[j-1][u]+b[j][i],f1[j-1][u-b[j][i]]));
}
}
for(int i=m;i>=1;i--)
{
for(int j=m;j>=i;j--)
{
for(int u=0;u<b[i][j];u++)f2[i][u]=max(f2[i][u],f2[j+1][u]+b[i][j]);
for(int u=b[i][j];u<=n;u++)f2[i][u]=max(f2[i][u],max(f2[j+1][u]+b[i][j],f2[j+1][u-b[i][j]]));
}
}
long long s=0,x;
for(int i=0;i<=n;i++)s=max(s,min(f1[m][i],(long long)i));
printf("%lld\n",s);
for(int i=1;i<=m;i++)
{
for(int j=i;j<=m;j++)
{
x=n;
for(int u=0;u<=n;u++)
{
while(x!=0&&f1[i-1][u]+f2[j+1][x]<x+u+b[i][j])x--;
f[i][j]=max(f[i][j],min(f1[i-1][u]+f2[j+1][x],x+u+b[i][j]));
}
}
}
for(int i=1;i<=n;i++)
{
s=0;
for(int j=1;j<=a[i].x;j++)
{
for(int u=a[i].y;u<=m;u++)s=max(s,f[j][u]);
}
printf("%lld\n",s);
}
return 0;
}
【CF1267G】 Game Relics
题目描述
有 \(n\) 个物品价值为 \(v_i\) ,有两种方式进行购买:
- 花费 \(v_i\) 购买第 \(i\) 个物品。
- 花 \(x\) 抽从 \(1\) 到 \(n\) 中随机抽一个物品,抽到重复的返还 \(\frac{x}{2}\) 。
求拥有所有物品所需的最小代价,\(1 \le n \le 100, 1 \le \sum_{i=1}^n v_i \le 10000,x \le v_i\) 。
解题思路
能得到性质:必定先抽后买,抽出相同数量元素的概率相同。
很容易想到一个假的做法:直接求出剩下 \(i\) 个直接买需要的期望代价,求出抽 \(i\) 个需要的代价,直接相加取最大。
但模拟一下样例就发现,抽的东西不一样,接下来的策略也会不一样。
由于抽出相同数量元素概率相同,所以相同数量,我们只需要考虑抽出东西的价值和来决定接下来是继续抽还是买。
但买和抽的操作不统一,我们可以把买转换成抽:在剩下的物品中抽出一个并付出 \(v_i\) 的代价。
这样我们就转化成了一个都是抽的问题。
对于 \(f_{i,j}\) 表示已拥有 \(i\) 个物品,总代价为 \(j\) 的概率,这个东西是一个类似 \(01\) 背包的东西,\(dp\) 解决时间复杂度 \(O(n^2 \sum_{i=1}^n v_i)\) 。
同时,我们考虑每一种情况下一步应该怎么走,对于 \(f_{i,j}\) 可以从全部抽,也可以从未抽出的抽,两种出一个的期望代价分别为 \(\frac{x}{2}\times (\frac{n}{n-i}+1)\) 与 \((\frac{(\sum_{i=1}^n v_i)-j}{n-i})\) ,只需要求哪个期望代价最小,乘上到这里概率 \(f_{i,j}\) 累加即可。
时间复杂度 \(O(n^2 \sum_{i=1}^n v_i)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
int n,k;
double m,a[105],d[105],g,s,f[105][10005];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i],k+=a[i];
for(int i=1;i<=n;i++)d[i]=(m/2)*(double(n)/double(n-i+1)+1);
g=1.00,s=m,f[0][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=k;j>=a[i];j--)
{
for(int u=1;u<=n;u++)f[u][j]+=f[u-1][int(j-a[i])]*double(u)/double(n-u+1);
}
}
for(int i=1;i<n;i++)
{
for(int j=0;j<=k;j++)s+=double(f[i][j])*min(d[i+1],double(k-j)/(n-i));
}
printf("%.17f",s);
return 0;
}
线段树&树状数组&莫队
【CF817F】 MEX Queries
题目描述
给一个 \(v_{1...10^{18}}\) ,有 \(3\) 种操作:
- 将 \(v_{l...r}\) 设为 \(1\) 。
- 将 \(v_{l...r}\) 设为 \(0\) 。
- 将 \(v_{l...r}\) 里面的数 \(01\) 翻转。
\(m\) 次操作,每次操作后求最小 \(k\) 使得 \(v_i=0\) ,\(1 \le m \le 10^5\)。
解题思路
用线段树维护。
离散化后用线段树维护一个区间 \(0\) 出现的最小位置,\(1\) 出现的最小位置,设 \(0/1\) 的操作直接修改最小位置,翻转则交换两个量即可。
时间复杂度 \(O(mlogm)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
struct que
{
long long p,l,r;
}b[100005];
long long n,dd[200005],min1[2000005],min0[2000005],d[2000005],re[2000005];
set<long long> l1;
map<long long,long long> p;
void galaxy1(long long x,long long l,long long r,long long v)
{
re[x]=0,d[x]=v;
if(v)min1[x]=l,min0[x]=1e9+5;
else min1[x]=1e9+5,min0[x]=l;
return;
}
void galaxy2(long long x,long long l,long long r)
{
if(d[x]!=-1)d[x]^=1;
re[x]^=1,swap(min1[x],min0[x]);
return;
}
void pushdown(long long x,long long l,long long r)
{
if(re[x]==0&&d[x]==-1)return;
long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
if(d[x]!=-1)
{
galaxy1(lc,l,mid,d[x]),galaxy1(rc,mid+1,r,d[x]),d[x]=-1,re[x]=0;
return;
}
galaxy2(lc,l,mid),galaxy2(rc,mid+1,r),re[x]=0;
return;
}
void build(long long x,long long l,long long r)
{
if(l==r)
{
min0[x]=l;
return;
}
long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
build(lc,l,mid),build(rc,mid+1,r);
min0[x]=l;
return;
}
void dijah1(long long x,long long l,long long r,long long ql,long long qr,long long v)
{
if(ql<=l&&r<=qr)
{
galaxy1(x,l,r,v);
return;
}
pushdown(x,l,r);
long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
if(ql<=mid)dijah1(lc,l,mid,ql,qr,v);
if(qr>mid)dijah1(rc,mid+1,r,ql,qr,v);
min1[x]=min(min1[x<<1],min1[(x<<1)|1]),min0[x]=min(min0[x<<1],min0[(x<<1)|1]);
return;
}
void dijah2(long long x,long long l,long long r,long long ql,long long qr)
{
if(ql<=l&&r<=qr)
{
galaxy2(x,l,r);
return;
}
pushdown(x,l,r);
long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
if(ql<=mid)dijah2(lc,l,mid,ql,qr);
if(qr>mid)dijah2(rc,mid+1,r,ql,qr);
min1[x]=min(min1[x<<1],min1[(x<<1)|1]),min0[x]=min(min0[x<<1],min0[(x<<1)|1]);
return;
}
int main()
{
scanf("%lld",&n);
l1.insert(1);
for(int i=1;i<=n;i++)scanf("%lld%lld%lld",&b[i].p,&b[i].l,&b[i].r),b[i].r++,l1.insert(b[i].l),l1.insert(b[i].r);
set<long long>::iterator q=l1.begin();
long long g=0;
for(;q!=l1.end();q++)p[*q]=++g,dd[g]=*q;
for(int i=1;i<=n;i++)b[i].l=p[b[i].l],b[i].r=p[b[i].r];
memset(d,0,sizeof(d)),memset(re,0,sizeof(re)),memset(min1,1,sizeof(min1));
build(1,1,2*n+1);
for(int i=1;i<=n;i++)
{
if(b[i].p==1)dijah1(1,1,2*n+1,b[i].l,b[i].r-1,1);
else if(b[i].p==2)dijah1(1,1,2*n+1,b[i].l,b[i].r-1,0);
else dijah2(1,1,2*n+1,b[i].l,b[i].r-1);
printf("%lld\n",(min0[1]>1e9?dd[g]:dd[min0[1]]));
}
return 0;
}
【Luogu P4479】 第k大斜率
题目描述
给出 \(n\) 个点,每两点直接连一条直线,求第 \(k\) 大的直线斜率,向下取整,\(1 \le n \le 10^5\) 。
解题思路
由于不要求精确,考虑二分。
设我们二分到 \(x\) ,就是判断有多少个点对 \(\frac{y_j-y_i}{x_j-x_i} \ge x\) ,与 \(k\) 比较进行二分。
拆一下式子,保证 \(x_j>x_i\) 的话,就是 \(y_j-x_j \times x \ge y_i-x_i \times x\) ,这个东西可以用树状数组做。
时间复杂度 \(O(nlog^2n)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long x,y;
}b[100005];
long long n,a[100005],d[100005],bb[100005],m,f[100005];
bool cmp1(datay q,datay w)
{
if(q.x!=w.x)return q.x<w.x;
return q.y>w.y;
}
long long lowbit(long long x)
{
return x&(-x);
}
void dijah(long long x,long long y)
{
for(int i=x;i<=n;i+=lowbit(i))f[i]+=y;
return;
}
long long gaia(long long x)
{
long long h=0;
while(x)h+=f[x],x-=lowbit(x);
return h;
}
long long check(long long k)
{
memset(bb,0,sizeof(bb));
long long x,l1,r1,mid1,q=0,s=0;
for(int i=1;i<=n;i++)d[i]=a[i]=b[i].y-k*b[i].x;
sort(d+1,d+n+1),d[0]=-1e13-5;
for(int i=1;i<=n;i++)bb[(d[i]!=d[i-1])?(++q):q]=d[i];
for(int i=1;i<=n;i++)
{
l1=1,r1=q,x=a[i],a[i]=0;
while(l1<=r1)
{
mid1=(l1+r1)>>1;
if(bb[mid1]<=x)a[i]=max(a[i],mid1),l1=mid1+1;
else r1=mid1-1;
}
}
for(int i=1;i<=n;i++)s+=gaia(a[i]),dijah(a[i],1);
for(int i=1;i<=n;i++)dijah(a[i],-1);
return s;
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)scanf("%lld%lld",&b[i].x,&b[i].y);
sort(b+1,b+n+1,cmp1);
long long l=-2e8,r=2e8,mid,s=-1e9-5;
while(l<=r)
{
mid=(l+r)>>1;
if(check(mid)>=m)s=max(s,mid),l=mid+1;
else r=mid-1;
}
printf("%lld",s);
return 0;
}
【Luogu P3899】 更为厉害
题目描述
给出一棵 \(n\) 个点的树,\(m\) 次询问,每次询问给出 \(x\) 和 \(k\) ,求有多少个 \(y,z\) 满足 \(x,y\) 都为 \(z\) 的祖先且 \(x,y\) 的距离小于等于 \(k\) ,\(1 \le n,m \le 3 \times 10^5\)。
解题思路
分两类情况考虑,\(y\) 是 \(x\) 的祖先或 \(x\) 是 \(y\) 的祖先。
\(y\) 是 \(x\) 祖先的情况很好考虑,取 \(y\) 能成为的节点数乘上 \(size_x\) 即可。
而 \(x\) 是 \(y\) 的祖先呢?我们需要找到 \(x\) 的子树内深度与 \(deep_x\) 差不超过 \(k\) 的节点并累加他们的 \(size\) ,这明显用主席树可以解决。
时间复杂度 \(O(mlogn)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long lc,rc,v;
}a[10000005];
long long n,m,dfn[300005],out[300005],num1,size[300005],deep[300005],num,re[300005],root[300005];
vector<long long> t[300005];
long long build(long long l,long long r)
{
if(l==r)
{
a[++num].v=0;
return num;
}
long long h=++num,mid=(l+r)>>1;
a[h].lc=build(l,mid),a[h].rc=build(mid+1,r);
return h;
}
long long dijah(long long x,long long l,long long r,long long k,long long v)
{
if(l==r)
{
a[++num]=a[x],a[num].v+=v;
return num;
}
long long h=++num,mid=(l+r)>>1;
a[h]=a[x];
if(k<=mid)a[h].lc=dijah(a[x].lc,l,mid,k,v);
else a[h].rc=dijah(a[x].rc,mid+1,r,k,v);
a[h].v=a[a[h].lc].v+a[a[h].rc].v;
return h;
}
long long gaia(long long x,long long l,long long r,long long ql,long long qr)
{
if(ql<=l&&r<=qr)return a[x].v;
long long mid=(l+r)>>1,h=0;
if(ql<=mid)h+=gaia(a[x].lc,l,mid,ql,qr);
if(qr>mid)h+=gaia(a[x].rc,mid+1,r,ql,qr);
return h;
}
void dfs(long long x,long long y)
{
deep[x]=deep[y]+1,dfn[x]=++num1,size[x]=1;
for(int i=0;i<t[x].size();i++)
{
if(t[x][i]==y)continue;
dfs(t[x][i],x),size[x]+=size[t[x][i]];
}
out[x]=num1,re[dfn[x]]=x;
return;
}
int main()
{
long long x,y;
scanf("%lld%lld",&n,&m);
for(int i=1;i<n;i++)scanf("%lld%lld",&x,&y),t[x].push_back(y),t[y].push_back(x);
dfs(1,0),root[0]=build(1,n);
for(int i=1;i<=n;i++)root[i]=dijah(root[i-1],1,n,deep[re[i]],size[re[i]]-1);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld",&x,&y);
printf("%lld\n",min(y,deep[x]-1)*(size[x]-1)+gaia(root[out[x]],1,n,deep[x]+1,deep[x]+y)-gaia(root[dfn[x]-1],1,n,deep[x]+1,deep[x]+y));
}
return 0;
}
【CF526F】 Pudding Monsters
题目描述
给出一个 \(n \times n\) 的网格,有 \(n\) 个棋子,每行每列恰好有一个棋子,求有多少个 \(k \times k\) 的正方形内有 \(k\) 个棋子,$ 1 \le n \le 3 \times 10^5$。
解题思路
因为每行每列恰有一个棋子,转化成一个长度为 \(n\) 的排列,求有多少个 \(1 \le l \le r \le n\) 满足 \(max(l,r)-min(l,r)=r-l\) 。
变一下式子,变成 \(max(l,r)-min(l,r)+l-r=0\) 。
设 \(f_i\) 表示以 \(i\) 为开始的区间的 \(max(i,r)-min(i,r)+i-r\) ,考虑在枚举 \(r\) 时动态维护这个数组。
\(i\) 不变,\(r\) 不断 \(+1\) ,所以每次都要给整个数组 \(-1\) 。
维护 \(max(i,r),min(i,r)\) 可以用单调栈,每次修改不断向前找直到找到 \(max/min\) 超过自己的,时间复杂度均摊是 \(O(nlogn)\) 的。
问题是维护等于 \(0\) 的数目不好维护,再次观察式子,发现它是恒大于等于 \(0\) 的,我们只需维护最小值的数目即可。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long minn,v;
}f[1200005];
long long n,a[300005],f1[300005],f2[300005],d[1200005];
stack<long long> l1,l2;
void galaxy(long long x,long long y)
{
f[x].minn+=y,d[x]+=y;
return;
}
void pushdown(long long x)
{
if(d[x]==0)return;
galaxy(x<<1,d[x]),galaxy((x<<1)|1,d[x]),d[x]=0;
return;
}
void build(long long x,long long l,long long r)
{
if(l==r)
{
f[x].minn=l,f[x].v=1;
return;
}
long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
build(lc,l,mid),build(rc,mid+1,r);
f[x].minn=min(f[lc].minn,f[rc].minn),f[x].v=(f[lc].minn==f[x].minn?f[lc].v:0)+(f[rc].minn==f[x].minn?f[rc].v:0);
return;
}
void dijah(long long x,long long l,long long r,long long ql,long long qr,long long v)
{
if(ql<=l&&r<=qr)
{
galaxy(x,v);
return;
}
pushdown(x);
long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
if(ql<=mid)dijah(lc,l,mid,ql,qr,v);
if(qr>mid)dijah(rc,mid+1,r,ql,qr,v);
f[x].minn=min(f[lc].minn,f[rc].minn),f[x].v=(f[lc].minn==f[x].minn?f[lc].v:0)+(f[rc].minn==f[x].minn?f[rc].v:0);
return;
}
int main()
{
long long x,y;
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld%lld",&x,&y),a[x]=y;
x=y=0,build(1,1,n);
for(int i=1;i<=n;i++)
{
while(l1.size()>0&&a[l1.top()]<a[i])l1.pop();
while(l2.size()>0&&a[l2.top()]>a[i])l2.pop();
if(l1.size()!=0)f1[i]=l1.top();l1.push(i);
if(l2.size()!=0)f2[i]=l2.top();l2.push(i);
}
long long s=0;
for(int i=1;i<=n;i++)
{
while(x!=0&&a[x]<a[i])dijah(1,1,n,f1[x]+1,x,-a[x]),x=f1[x];
while(y!=0&&a[y]>a[i])dijah(1,1,n,f2[y]+1,y,a[y]),y=f2[y];
dijah(1,1,n,x+1,i,a[i]),dijah(1,1,n,y+1,i,-a[i]),dijah(1,1,n,1,n,-1);
s+=f[1].v,x=i,y=i;
}
printf("%lld",s);
return 0;
}
【Luogu P3245】 大数
题目描述
给出一个 \(n\) 位的数 \(S\) ,每一次查询求有多少个 \(l \le i \le j \le r\) ,使得 \(S\) 中第 \(i\) 到第 \(j\) 位组成的数为 \(k\) 的倍数,所有询问的 \(k\) 不变且为素数,\(1 \le n \le 2 \times 10^5\) 。
解题思路
设 \(g_i\) 从 \(S\) 的第 \(i\) 位到最后一位组成的数,那么若一个区间 \([l,r]\) 满足条件,有 \(k | \frac{g_l-g_{r+1}}{10^{r-l+1}}\) 。
若 \(k\) 为除 \(2,5\) 以外的素数,那么有 \(k | g_l-g_{r+1}\) ,即为 \(g_l \% k =g_{r+1} \% k\) ,莫队模板。
若 \(k\) 为 \(2,5\) ,前缀和预处理后直接求即可。
时间复杂度 \(O(n \sqrt n)\) 。
Code
#include<bits/stdc++.h>
using namespace std;
struct datay
{
long long x,y,p,z;
}a[200005];
long long k,n,m,p,f[200005],d[200005],s;
set<long long> ll;
map<long long,long long> qwe;
string b;
bool cmp1(datay q,datay w)
{
if(q.x/p!=w.x/p)return q.x<w.x;
return q.y<w.y;
}
bool cmp2(datay q,datay w)
{
return q.p<w.p;
}
void add(long long x)
{
s+=d[x],d[x]++;
return;
}
void del(long long x)
{
d[x]--,s-=d[x];
return;
}
void gaia1()
{
for(int i=1;i<=n;i++)f[i]=f[i-1]+((b[i]-'0')%2==0),d[i]=d[i-1]+((b[i]-'0')%2==0)*i;
for(int i=1;i<=m;i++)printf("%lld\n",d[a[i].y]-d[a[i].x-1]-(f[a[i].y]-f[a[i].x-1])*(a[i].x-1));
return;
}
void gaia2()
{
for(int i=1;i<=n;i++)f[i]=f[i-1]+((b[i]-'0')%5==0),d[i]=d[i-1]+((b[i]-'0')%5==0)*i;
for(int i=1;i<=m;i++)printf("%lld\n",d[a[i].y]-d[a[i].x-1]-(f[a[i].y]-f[a[i].x-1])*(a[i].x-1));
return;
}
void gaia3()
{
p=sqrt(n);
sort(a+1,a+m+1,cmp1);
long long l=1,r=0,qw=1;
for(int i=n;i>=1;i--)f[i]=(f[i+1]+(b[i]-'0')*qw)%k,qw=(qw*10)%k;
for(int i=1;i<=n+1;i++)ll.insert(f[i]);
long long g=0;
set<long long>::iterator q=ll.begin();
for(;q!=ll.end();q++)qwe[*q]=++g;
for(int i=1;i<=n+1;i++)f[i]=qwe[f[i]];
for(int i=1;i<=m;i++)
{
while(r<a[i].y+1)r++,add(f[r]);
while(r>a[i].y+1)del(f[r]),r--;
while(l<a[i].x)del(f[l]),l++;
while(l>a[i].x)l--,add(f[l]);
a[i].z=s;
}
sort(a+1,a+m+1,cmp2);
for(int i=1;i<=m;i++)printf("%lld\n",a[i].z);
return;
}
int main()
{
scanf("%lld",&k),cin>>b,scanf("%lld",&m);
n=b.size(),b=' '+b;
for(int i=1;i<=m;i++)scanf("%lld%lld",&a[i].x,&a[i].y),a[i].p=i;
if(k==2)gaia1();
else if(k==5)gaia2();
else gaia3();
return 0;
}