dp 优化部分例题

请忽视标题并膜 plate_let 分钟

Dice Product 2 (Atcoder ABC245 Ex)

题意

有一个变量 x 初始为 1。可以进行若干次操作,每次可以给 x 乘上一个 [1,n] 内一个随机整数,求使 x 大于 m 的期望操作次数。n,m109

解法

dpi 为使 x 大于 i 的期望次数,则关系有

dp0=1

dpi=j=1ndpij+1n=dpin+j=2ndpij+nn=j=2ndpij+nn1

考虑 ij 最多只会有 O(n) 种,可以使用整除分块优化。

定理:abc=abc

证明:令 x1abx2abc,则 x1=maxbxaxx2=max(cx)bax,则 abc=maxcxx1x=max(cx)ba=x2,故 abc=abc

由上述定理可得:使用记忆化搜索,则 dpi 只与 dpij(j[1,i]) 有关。

定理 2:计算一次 dpM 的时间复杂度是 O(M34) 的。

证明:使用整除分块优化,则在已知 dpij(j[1,i]) 时,计算 dpi 的单次时间复杂度是 O(i) 的,故而令 S=M,则计算 dpM 的复杂度为 O(i=1Si+i=1SMi)。前者不超过 O(SS)=O(M34),后者

O(i=1SMi)=O(1SMxdx)=O(M1S1xdx)=O(M1Sx12dx)=O(M(S12112))=O(M12M14)=O(M34)

故而计算 dpM 的总复杂度为 O(M34)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int md=1000000007;
#define ll long long
int n,m,inv;
inline ll Pow(ll d,int z){
ll ret=1;
do{
if(z&1){ret*=d;if(ret>=md) ret%=md;}
d*=d;if(d>=md) d%=md;
}while(z>>=1);
return ret;
}
unordered_map<int,int> Mp;
int dp(const int x){
if(Mp.find(x)!=Mp.end()) return Mp[x];
int v,l,r;ll ans=n;
const int a=min(n,x);
for(l=2;l<=a;l=r+1){
v=x/l;r=x/v;
ans+=(min(r,n)-l+1ll)*dp(x/l);
if(ans>=md) ans%=md;
}
ans*=inv;
if(ans>=md) ans%=md;
return Mp[x]=ans;
}
int main(){
Mp[0]=1;
scanf("%d%d",&n,&m);
inv=Pow(n-1,md-2);
printf("%d",dp(m));
return 0;
}

划分 (CSP-S 2019)

题意

给定一个长为 n 的正整数数组 a,要求将其划为若干子串,要求子串和单调不降,求所有子串最小平方和。n4×107

解法

由于完全平方公式 (a+b)2>a2+b2(a,b>0),故而需要尽可能多地进行分段。

考虑下面一种情况:

上面若 iLai+xiRai,则 x 被分到 L 段会使得答案更优。

证明:

(iLai+x)2+(iRai)2=(iLai)2+(iRai)2+2xiLai+x2

(iRai+x)2+(iLai)2=(iRai)2+(iLai)2+2xiRai+x2

由于 iRai>iLai,故将 x 分至 L 段一定更优。

故而可以得出一种贪心构造:对于某一段的划分方案,需要贪心地使最后一段的和尽可能小,这样既保证了上面的推论和分段尽可能多的推论,同时保证了下一段的和尽可能小。

1i 段的最优划分中以 gi+1i 为一段,sij=1iaj,则转移有 gimaxj=1i1j[sisjsjsgj],也就是 gimaxj=1i1j[si2sjsgj]。此时令 vi=2sisgi,则 gimaxj=1i1j[sivj]

可以发现若 j,k[1,i1],且 vkvj,j<k,则 p[i,n],有 spvjspvk,故 j 永远不会优于 k

故而在转移时可以用单调队列,保证 v 值单增,计算 gi 前在队头弹掉 j(vjsi),取最后一个弹掉的 j 作为 gi,然后在队尾弹掉 j(vjvi),再将 i 入队。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=40000010;
const int maxm=100010;
int n,i,j=1,m,lt,rt;
int l[maxm],r[maxm],p[maxm];
int q[maxn],g[maxn];
ll x,y,z,s[maxn],v[maxn];
bool t;__int128_t ans,tmp;
void Write(const __int128_t a){
if(a>9) Write(a/10);
putchar('0'+(a%10));
}
int main(){
scanf("%d%d",&n,&t);
if(t){
scanf("%lld%lld%lld%lld%lld%d",&x,&y,&z,s+1,s+2,&m);
for(i=1;i<=m;++i) scanf("%d%d%d",p+i,l+i,r+i);
for(i=3;i<=n;++i) s[i]=(x*s[i-1]+y*s[i-2]+z)&((1<<30)-1);
for(i=1;i<=n;++i){
if(i>p[j]) ++j;
s[i]=(s[i]%(r[j]-l[j]+1))+l[j];
}
}
else for(i=1;i<=n;++i) scanf("%lld",s+i);
lt=rt=1;
for(i=1;i<=n;++i){
s[i]+=s[i-1];j=q[lt+1];
while(lt<rt&&v[j]<=s[i]) j=q[(++lt)+1];
g[i]=j=q[lt];v[i]=(s[i]<<1LL)-s[j];j=q[rt];
while(lt<=rt&&v[j]>=v[i]) j=q[--rt];
q[++rt]=i;
}
for(i=n;i;i=g[i]){
tmp=s[i]-s[g[i]];
ans+=tmp*tmp;
}
Write(ans);return 0;
}

FoxSearchingRuins (Topcoder srm501 C)

题意

有一个 W×H 的网格,同时有 n 个珠宝,第 i 个珠宝在 (xi,yi) 处,价值为 ci。你可以从第一行任意处开始搜寻珠宝。每次只可以向下/左/右移动一格,向下移动一格耗时 ty,向左右移动一格耗时 tx,只允许左右移 LR 次。求至少收集总价值为 g 的珠宝的最少时间(采集珠宝不需要时间)或得出无解。W,n,LR1000,H109

解法

首先假设不存在横坐标相等的点,将所有点按照纵坐标升序排序。考虑 dp。

dpi,j 表示在第 i 个点,左右走了 j 步的最大价值。显然时间为 tyyi+txj,坐标为 (xi,yi)。对于 bi,可得 dpi,j=max(dpb,j|xbxi|)+ci。将绝对值拆开,可得 dpi,j=ci+{maxdpb,j+xbxi(xixb)maxdpb,j+xixb(xixb) 或是 dpi,j+xi=ci+maxxbxidpb,j+xbdpi,jxi=ci+maxxbxidpb,jxb

可以使用树状数组快速统计前缀/后缀最大值。具体来说,在第 jxb 个维护前缀最大值的树状数组的 xb 处插入 dpb,j,在第 j+xb 个维护后缀最大值的树状数组的 xb 处插入 dpb,j。计算 dpi,j 时查询第 jxi 个树状数组的 xi 的前缀和第 j+xi 个树状数组的后缀。具体实现可能出入较大。

考虑横坐标相同的情况。我们需要避免在一行内来回走的情况,可以发现如果正确处理了其他的 dp 值,则某行内来回走的情况可以处理成如下形式:

故而计算 dp 值时,每次处理一行的点的 dp 值,处理完后复制一份这些 dp 值,两份分别从左往右、从右往左进行转移,最后进行合并,即可保证正确性。时间复杂度为 O(nWlogW)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1010;
int i,j,w,lp,rp,rk;
struct node{
int x,y;
long long v;
inline bool operator <(const node &a)const{
if(x!=a.x) return x<a.x;
return y<a.y;
}
}N[maxn],C[maxn];
struct BIT{
long long c[maxn];
inline void Chg(int p,const long long &d){
for(;p<=w;p+=p&-p) if(c[p]<d) c[p]=d;
}
inline long long Que(int p){
long long ret=0;
for(;p;p^=(p&-p)) ret=max(ret,c[p]);
return ret;
}
}A[maxn<<1],B[maxn<<1];
long long ans=LLONG_MAX,dp[maxn][maxn],tp[maxn][maxn];
class FoxSearchingRuins{
public:
long long theMinTime(int W,int H,int jewelCount,int LR,int goalValue,int timeX,int timeY,vector<int> seeds){
N[jewelCount+1]={1145141919,1145141919,0};
N[1].y=(1LL*seeds[0]*seeds[1]+seeds[2])%W+1;
N[1].x=(1LL*seeds[3]*seeds[4]+seeds[5])%H+1;
N[1].v=(1LL*seeds[6]*seeds[7]+seeds[8])%seeds[9];
for(i=2;i<=jewelCount;++i){
N[i].y=(1LL*(N[i-1].y-1)*seeds[1]+seeds[2])%W+1;
N[i].x=(1LL*(N[i-1].x-1)*seeds[4]+seeds[5])%H+1;
N[i].v=(1LL*N[i-1].v*seeds[7]+seeds[8])%seeds[9];
}
sort(N+1,N+jewelCount+1);
C[1]=N[1];rk=1;w=(W+=5);
for(i=2;i<=jewelCount;++i){
while(!(N[i-1]<N[i])) C[rk].v+=N[i++].v;
if(i>jewelCount) break;C[++rk]=N[i];
}
for(lp=rp=1;lp<=rk;lp=++rp){
while(C[rp].x==C[lp].x) ++rp;--rp;
for(i=lp;i<=rp;++i)
for(j=0;j<=LR;++j)
tp[i][j]=dp[i][j]=max(A[j-C[i].y+W].Que(C[i].y),
B[j+C[i].y].Que(W-C[i].y))+C[i].v;
for(i=lp+1;i<=rp;++i)
for(j=C[i].y-C[i-1].y;j<=LR;++j)
dp[i][j]=max(dp[i][j],dp[i-1][j-C[i].y+C[i-1].y]+C[i].v);
for(i=rp-1;i>=lp;--i)
for(j=C[i+1].y-C[i].y;j<=LR;++j)
tp[i][j]=max(tp[i][j],tp[i+1][j-C[i+1].y+C[i].y]+C[i].v);
for(i=lp;i<=rp;++i){
for(j=0;j<=LR;++j){
if(tp[i][j]>dp[i][j]) dp[i][j]=tp[i][j];
A[j-C[i].y+W].Chg(C[i].y,dp[i][j]);
B[j+C[i].y].Chg(W-C[i].y,dp[i][j]);
}
}
}
for(i=1;i<=rk;++i){
for(j=0;j<=LR;++j){
if(dp[i][j]<goalValue) continue;
ans=min(ans,(C[i].x-1LL)*timeY+1LL*timeX*j);
}
}
if(ans==LLONG_MAX) ans=-1;
return ans;
}
};

HOT-Hotels 加强版 (POI 2014)

题意

给定一棵大小为 n 的树,求两两距离相等的三个点的无序三元组有多少组。n105

解法

考虑这三个两两距离相同的点有哪些共同之处。发现这三个点两两距离相等时(情况如下),它们之间必有一个点有与它们之间有长度相同且不重的路径。

同时由于这是一棵树,所以我们在进入某个点上时,需要维护通过有多少个点有通过其祖先到其的长为 x 的简单路径,显然其不好维护。

可以换一种方式进行维护。我们分别考虑这三种情况具体有哪些更方便维护,能够在这些路径上的最高点进行统计的关系。

第一种情况中有 u 的三棵不同的子树中均有有与 u 距离相等的点。

第二种情况中有 x 的子树中存在 xy 满足 disu,y=disu,z=disu,xdislca(y,z),y=dislca(y,z),z=dislca(y,z),x+0

第三种情况中有 w 的两棵子树中,有 disu,y=disu,z=disu,x,即 dislca(y,z),y=dislca(y,z),z=dislca(y,z),x=dislca(y,z),w+disw,x

故而可以维护 fi,ji 的子树中 disx,i=jx 数量,gi,ji 的子树中 dislca(x,y),x=dislca(x,y),y=dislca(x,y),i+j 的无序二元组 {x,y} 的数量,则答案为 i=1n(gi,0+j>0,x,y,zsonifx,j1(fy,j1fz,j1+gy,j+1))。同时转移方式为

fi,0=1,fi,j=xsonifx,j1

gi,j=x,ysoni(gx,j+1+fx,j1fy,j1)

由于 n105,上述朴素转移的复杂度不会低于 O(n2),考虑优化转移。考虑依次将 soni 的各个元素统计入 i 的数据的过程。可以先维护某个时刻的 fu,jgu,j,然后把新的 fx,j 合并到目前统计的答案和 fi,j,gi,j 中。同时这个思路的时间复杂度和空间复杂度仍然可能是 O(n2) 的(树近似一条链),因为 j 的最大值为子树深度。继续考虑优化。

si 所在长链的后继,我们发现 fi,j 可以赋初值为 fs,j1,同时 gi,j 可以赋初值为 gs,j+1。使用指针分配内存和指针的左右移可以在 O(1) 的时间内转移,最后需要转移涉及到的 j 值不会多于长儿子的子树深度减去该点深度。可以证明总复杂度是线性的。

注意分配内存时记了 gs=gi1,fs=fi+1,故而需要对 f 开两倍空间(其中后半部分空间用于 g)。g 不必开两倍空间。具体见代码。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=100010;
int n,i,u,v,t;
int h[maxn],mx[maxn],son[maxn],dep[maxn];
struct edge{int to,nxt;}E[maxn<<1];
ll V[maxn*3],ans;
ll *f[maxn],*g[maxn],*pt=V;
void dfs(const int &p){
int lp,to,md=0;
for(lp=h[p];lp;lp=E[lp].nxt){
to=E[lp].to;
if(dep[to]) continue;
dep[to]=dep[p]+1;
dfs(to);
if(mx[to]>md){
md=mx[to];
son[p]=to;
}
}
mx[p]=mx[son[p]]+1;
}
void dp(const int &p){
int lp,to=son[p],li;
if(to){
f[to]=f[p]+1;
g[to]=g[p]-1;
dp(to);
}
f[p][0]=1;
ans+=g[p][0];
for(lp=h[p];lp;lp=E[lp].nxt){
to=E[lp].to;
if(f[to]) continue;
f[to]=pt;pt+=(mx[to]<<1);
g[to]=pt;pt+=mx[to];
dp(to);ans+=g[p][1]*f[to][0];
for(li=1;li<mx[to];++li){
ans+=g[p][li+1]*f[to][li];
ans+=f[p][li-1]*g[to][li];
}
g[p][1]+=f[p][1]*f[to][0];
f[p][1]+=f[to][0];
for(li=1;li<mx[to];++li){
g[p][li+1]+=f[p][li+1]*f[to][li];
g[p][li-1]+=g[to][li];
f[p][li+1]+=f[to][li];
}
}
}
int main(){
scanf("%d",&n);
for(i=1;i<n;++i){
scanf("%d%d",&u,&v);
E[++t]={v,h[u]};h[u]=t;
E[++t]={u,h[v]};h[v]=t;
}
dep[1]=1;dfs(1);
f[1]=pt;pt+=(mx[1]<<1);
g[1]=pt;pt+=mx[1];
dp(1);printf("%lld",ans);
return 0;
}

星座 3 (JOISC 2020)

题意

有一张 n×n 的图片,在第 i 列第 1ai 行为白色。并且有 m 个星星,第 i 个在第 xi 行第 yi 列,删除代价为 ci。现在需要删除若干星星,满足不存在一个不包含白色区域但包含一个以上的星星。求删除星星的最小代价和。n,m2×105,没有位置相同的星星。

解法

考虑从下往上一次考虑每一行的星星需不需要删。

如果在目前这一行星星所在的一列的在这一行连通的列(指之间没有白色像素隔开的所有列)中有行数不大于当前行的某些星星,则这颗星星和这些所有星星均会构成矩形。对应需要立即删去某一个星星或已经保留的所有星星。可以直接贪心删掉代价小者,这样做可以发现是对的。

至于统计连通块,可以使用并查集。统计目前已经保留的星星时,可以使用树状数组维护区间加,加入一颗星星对应在这颗星星对应的整个连通区域加上这颗星星的代价。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
#define ll long long
int n,x,y,c,m,i,j;
ll v,ans,C[maxn];
inline void Add(int p,const ll d){
for(;p<maxn;p+=(p&-p)) C[p]+=d;
}
inline ll query(int p){
ll ret=0;
for(;p;p^=(p&-p)) ret+=C[p];
return ret;
}
int ht[maxn],st[maxn],nxt[maxn];
struct s{int xt,ct,nxt;}S[maxn];
struct dsu{
int fa[maxn];
dsu(){for(j=1;j<maxn;++j) fa[j]=j;}
int Find(const int p){
if(p==fa[p]) return p;
return fa[p]=Find(fa[p]);
}
}la,ra;
int main(){
scanf("%d",&n);
for(i=1;i<=n;++i){
scanf("%d",&c);
nxt[i]=ht[c];ht[c]=i;
}
scanf("%d",&m);
for(i=1;i<=m;++i){
scanf("%d%d%d",&x,&y,&c);
S[i]={x,c,st[y]};st[y]=i;
}
for(i=1;i<=n;++i){
for(j=st[i];j;j=S[j].nxt){
x=S[j].xt;c=S[j].ct;
v=query(x);
if(v<c){
ans+=v;
Add(la.Find(x)+1,c-v);
Add(ra.Find(x),v-c);
}
else ans+=c;
}
for(j=ht[i];j;j=nxt[j]){
la.fa[j]=j-1;
ra.fa[j]=j+1;
}
}
printf("%lld\n",ans);
return 0;
}

Problem Scores (Atcoder AGC041 D)

题意

你需要给一个长为 n 的正整数数列 a 赋值,满足每个数不大于 n 且非严格单增,并且任意 k+1 个数的和大于任意 k 个数的和。求方案数对 p 取模。n5000

解法

题目等效于把题目升序排好序后后 k 个数之和一定小于前 k+1 个数之和。

kn12 时后 k 个数与前 k+1 个数不重合,否则去掉两者重合的部分,其选择的前缀长度一定不大于 n12。同时当一个前缀和一个后缀不重时,可以将前缀和后缀均作延伸,前缀增加的数一定不大于后缀增加的数。故而只需要讨论 k=n12 的情况。

显然所有数初始均为 n 时任意 k 个数之和一定为任意 k+1 个数之和减 n,同时如果最小数不大于 0 则一定不能满足上述条件。

考虑对一些前缀减去 1,最后一定可以得到所有的不降序列。然而每次对某个前缀减去 1 后需要考虑其对 i=1n12+1aii=nn12+1nai 的减少量,因为总的减少量不能超过 n1,否则不能满足题目中的条件。

可以抽象成 01 背包问题的求方案数,方案数即为问题的解。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5010;
int n,p,i,j,s;
int w[maxn],dp[maxn];
int main(){
scanf("%d%d",&n,&p);
for(i=1;i<=n;++i) w[i]=i;
for(i=1;(i<<1)<=n;++i) w[n-i+1]=i;
dp[0]=1;
for(i=1;i<=n;++i){
for(j=w[i];j<n;++j){
dp[j]+=dp[j-w[i]];
if(dp[j]>=p) dp[j]-=p;
}
}
for(i=0;i<n;++i){
s+=dp[i];
if(s>=p) s-=p;
}
printf("%d\n",s);
return 0;
}

Many Moves (Atcoder ARC073 D)

题意

有一个长为 n 的数轴。数轴上开始时有两枚棋子分别位于 a,b 处。依次给出 q 组请求,第 i 次请求需要你把某棋子移到 xi 处。求棋子总共移动的路程的和的最小值。q,n2×105

解法

考虑 dp 转移。设 dpi,j 表示在满足第 i 次要求后两个棋子分别在 {xi,j} 的最小代价,转移有:

dpi+1,xi=minj=1n(dpi,j+|xij|)=min(minj=1xi((dpi,jj)+xi),minj=xi+1n((dpi,j+j)xi))

dpi+1,j=min(dpi+1,j,dpi,j+|xi+1xi|)

可以使用滚动数组进行优化。

有一个式子具体需要对 minj=1xi(dpjj)minj=xi+1n(dpj+j) 统计,可以直接在维护 dpj 时顺便处理 min(dpjj)min(dpj+j)。可以使用树状数组或线段树维护。

初值可以定为 x0=adpi=|jb|

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
#define ll long long
int n,q,a,b,x;
int to[maxn];
ll que,qua,qub,tmp;
struct seg{
int l,r;
ll mn,ma,mb,add;
}tr[maxn<<2];
#define l(p) tr[p].l
#define r(p) tr[p].r
#define m(p) ((l(p)+r(p))>>1)
#define ls(p) p<<1
#define rs(p) p<<1|1
#define mn(p) tr[p].mn
#define ma(p) tr[p].ma
#define mb(p) tr[p].mb
#define add(p) tr[p].add
inline void Pushup(const int &p){
mn(p)=min(mn(ls(p)),mn(rs(p)));
ma(p)=min(ma(ls(p)),ma(rs(p)));
mb(p)=min(mb(ls(p)),mb(rs(p)));
}
void build(const int p,const int l,const int r){
l(p)=l;r(p)=r;
if(l==r){
if(l<b) mn(p)=b-l;
else mn(p)=l-b;
ma(p)=mn(p)+l;
mb(p)=mn(p)-l;
to[l]=p;return;
}
build(ls(p),l,m(p));
build(rs(p),m(p)+1,r);
Pushup(p);
}
inline void Pushdown(const int &p){
tmp=add(p);if(!tmp) return;
mn(ls(p))+=tmp;mn(rs(p))+=tmp;
ma(ls(p))+=tmp;ma(rs(p))+=tmp;
mb(ls(p))+=tmp;mb(rs(p))+=tmp;
add(ls(p))+=tmp;add(rs(p))+=tmp;
add(p)=0;
}
void change(const int p,const int &pos){
if(l(p)==r(p)){
mn(p)=que;
ma(p)=que+pos;
mb(p)=que-pos;
return;
}
Pushdown(p);
if(pos<=m(p)) change(ls(p),pos);
else change(rs(p),pos);
Pushup(p);
}
void quea(const int p,const int &l){
if(l<=l(p)){
if(qua>ma(p)) qua=ma(p);
return;
}
Pushdown(p);quea(rs(p),l);
if(l<=m(p)) quea(ls(p),l);
}
void queb(const int p,const int &r){
if(r>=r(p)){
if(qub>mb(p)) qub=mb(p);
return;
}
Pushdown(p);queb(ls(p),r);
if(r>m(p)) queb(rs(p),r);
}
int main(){
scanf("%d%d%d%d",&n,&q,&a,&b);
build(1,1,n);
while(q--){
scanf("%d",&x);
qua=qub=11451419198101926;
quea(1,x);queb(1,x);
que=min(qua-x,qub+x);
b=x-a;if(b<0) b=-b;
add(1)+=b;mn(1)+=b;
ma(1)+=b;mb(1)+=b;
change(1,a);a=x;
}
printf("%lld",mn(1));
return 0;
}

Trinity (Atcoder AGC021F)

题意

有一个 n×m 的全白矩阵。可将任意位置染黑,之后用 a,b,c 三个数组表示此矩阵:

  • ai 表示第 i 行第 j 列为黑的最小 j(不存在则为 m+1)。
  • bi 表示第 k 行第 i 列为黑的最小 k(不存在则为 n+1)。
  • ci 表示第 k 行第 i 列为黑的最大 k(不存在则为 0)。

求所有可能的有序三元组 (a,b,c) 个数,对 998244353 取模。n8×103,m200

解法

dpi,j 表示在 ij 列的矩阵内,每行中至少有一个黑格的 {a,b,c} 个数。此时答案为 k=0idpk,m(nk)

令考虑到的是 ij 列可能有黑格的情况。我们考虑 a 中有多少个 j,即每一行中有多少个只在最后一列的黑格。显然只有最后一列有的黑格体现在了 a 上,且之前的列一定在这一列黑格处没有黑格,可以删掉对应的行。

考虑最后一列的黑格的分布以及其对 b,c 的影响:

  • 无黑格。此时 bc 对应取值只有 1 种。
  • 有一个黑格。此时 bc 取值相同,有 i 种。
  • 有至少两个黑格。此时 bc 取值不同,有 i(i1)2 种(其中在第 (bj,cj) 行的黑格不会统计在 b,c 中)

至此有 dpi,jdpi,j+dpi,j1(1+i+i(i1)2)(最后一列没有专有的黑格)。

如果最后一列有独立的黑格,则令j1 列没有黑格的行数总共为 k,其中黑格行数最大为 y,最小为 x;则 bi[1,x] 中选出,ci[y,i] 中选出。注意 x y 行一定为黑格,故而考虑到 bi 时,可以是在 [1,x) 中选一个,可以是不在 [1,x) 中(就是在第 x 行),ci 同理。

考虑将 bici[x,y] 中独立。可以考虑将 bi[1,x] 中选出即将 bi1[0,x1] 中选出,同理将 ci[y,i] 中选出即将 ci+1[y+1,i+1] 中选出,最后在 (x,y) 中选出 k2 个黑格。

如果我们直接在 [0,i+1] 中选出任意 k+2 个数,则将第 2 个数视为上述的 x,第 k2 个数视为上述的 y;中间即是在 [x+1,y1] 中选出 k2 个数,两边即是在 [0,x1][y+1,i+1] 中各选出一个数。这样在 [0,i+1] 中选出任意 k+2 个数即与上述的选择方式有一一对应的关系,上述选择的方式即有 (i+2k+2)

综上,dpi,j 转移即有

dpi,j=dpi,j1(1+i+i(i1)2)+k=1idpik,j1(i+2k+2)=dpi,j1(i+i+i(i1)2)+k=1idpik,j1(i+2ik)=dpi,j1(i+i+i(i1)2)+k=1idpk,j1(i+2k)

如果直接这样进行转移,则复杂度会是 O(n2m) 的。

考虑优化,发现复杂度主要与 k=1idpk,j1(i+2k) 的计算有关。考虑拆开 k=1idpk,j1(i+2k) 可得

k=1idpk,j1(i+2k)=k=1idpk,j1(i+2)!k!(i+2k)!=k=1i(dpk,j11k!)((i+2)!(i+2k)!)

g1(x)=dpk,j11x!g2(x)=(i+2)!(x+2)!;则 k=1idpk,j1(i+2k)=k=1ig1(k)g2(ik)

可以发现这是一个卷积的形式(不了解卷积者可见下):

构造多项式 G1(x)=k=1ng1(k)xkG2(x)=k=1ng2(k)xk;则

G1(x)G2(x)=i=12n1xi+1(k=1ig1(k)g2(ik))=i=12n1xi+1(k=1idpk,j1(i+2k))

此时 xi+1 的系数即为 dpi,jdpi,j1(1+i+i(i1)2)。直接使用 NTT(支持整数系数和取模)计算 i[1,n]dpi,j 即可,单次计算为 O(nlogn)。下面的代码可能会有所不同。

听说有用 EGF 的 O(n+m3) 做法,然而作者不会。

p.s. 对于形如 h(i)=i=1nf(i)g(ni) 的形式,一般都可以按照上述方式使用 FFT/NTT 优化计算。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=8010;
const int maxm=210;
const int maxl=32800;
const ll md=998244353;
const ll G=3,InvG=332748118;
int n,m,i,j,_i,_j,_k,lm=1,le;
int r[maxl];
ll w,x,y,wn,wp;
ll dp[maxm][maxn];
ll g1[maxl],g2[maxl];
ll fac[maxn],inv[maxn];
inline ll Pow(ll d,int z){
ll ret=1;
do{
if(z&1){
ret*=d;
if(ret>=md) ret%=md;
}
d*=d;
if(d>=md) d%=md;
}while(z>>=1);
return ret;
}
inline ll C(int x,int y){
if(x>y) return 0;
ll ret=fac[y]*inv[x];
if(ret>=md) ret%=md;
ret*=inv[y-x];
if(ret>=md) ret%=md;
return ret;
}
inline void NTT(ll *c,const ll p){
for(_i=1;_i<lm;++_i) if(_i<r[_i]) swap(c[_i],c[r[_i]]);
for(_i=le=1;le<lm;++_i,le<<=1){
wn=Pow(p,(md-1)>>_i);
for(_j=0;_j<lm;_j+=(le<<1)){
w=1;
for(_k=0;_k<le;++_k){
x=c[_j+_k];
y=w*c[_j+_k+le];
if(y>=md) y%=md;
x+=y;if(x>=md) x-=md;
c[_j+_k]=x;
x-=(y<<1LL);
if(x<0) x+=md;
if(x<0) x+=md;
c[_j+_k+le]=x;
w*=wn;if(w>=md) w%=md;
}
}
}
}
int main(){
fac[1]=inv[0]=1;
for(i=2;i<maxn;++i){
fac[i]=fac[i-1]*i;
if(fac[i]>=md) fac[i]%=md;
}
inv[maxn-1]=Pow(fac[maxn-1],md-2);
for(i=maxn-2;i;--i){
inv[i]=inv[i+1]*(i+1);
if(inv[i]>=md) inv[i]%=md;
}
scanf("%d%d",&n,&m);
while(lm<=2*n){lm<<=1;++le;}
for(i=1;i<lm;++i){
r[i]=r[i>>1]>>1;
if(i&1) r[i]|=(lm>>1);
}
wp=Pow(lm,md-2);
dp[0][0]=1;
for(i=1;i<=m;++i){
memset(g1,0,sizeof(g1));
memset(g2,0,sizeof(g2));
for(j=0;j<=n;++j){
w=j+1+(((j-1LL)*j)>>1);
if(w>=md) w%=md;
dp[i][j]=dp[i-1][j]*w;
if(dp[i][j]>=md) dp[i][j]%=md;
g1[j]=dp[i-1][j]*inv[j];
if(g1[j]>=md) g1[j]%=md;
g2[j]=inv[j+2];
}
g2[0]=0;
NTT(g1,G);NTT(g2,G);
for(j=0;j<lm;++j){
g1[j]*=g2[j];
if(g1[j]>=md) g1[j]%=md;
}
NTT(g1,InvG);
for(j=1;j<=n;++j){
w=g1[j]*wp;if(w>=md) w%=md;
w*=fac[j+2];if(w>=md) w%=md;
dp[i][j]+=w;
if(dp[i][j]>=md) dp[i][j]-=md;
}
}
w=0;
for(i=0;i<=n;++i){
w+=dp[m][i]*C(i,n);
if(w>=md) w%=md;
}
printf("%lld",w);
return 0;
}

邮局 (IOI 2000)

题意

一个数轴上有 v 个村庄,两两坐标不同,第 i 个的坐标为 xi。要求在 p 个村庄中建立邮局,求每个村庄和最近邮局之间的距离的和的最小值。p300,v3000

解法

首先列出朴素的 dp 式:

dpi,j 表示在前 j 个村庄中设立 i 个邮局的最小代价,转移有:

dpi,j=mink=0j1(dpi1,k+W(k+1,j))

其中 W(l,r) 表示在村庄 [l,r] 之间有刚好一个邮局时的最小代价。易得在 l+r2l+r2 村庄处建立邮局最佳。

由定义得 W(l,r+1)=W(l,r)+xr+1xl+r2,初值为 W(i,i)=0

考虑证明 dp 满足四边形不等式,则需要证明 W 满足四边形不等式。

证明:

由定义可得 W(l,r+1)=W(l,r)+xr+1xl+r2W(l1,r)=W(l,r)+xl+r2xl1W(l1,r+1)=W(l1,r)+xr+1xl+r12=W(l,r)+xl+r2xl+r12+xr+1xl1,故而

W(l,r+1)+W(l1,r)=2W(l,r)+xr+1xl1+xl+r2xl+r2

W(l,r)+W(l1,r+1)=2W(l,r)+xl+r2xl+r12+xr+1xl1

而又因为 xl+r12xl+r2,所以 W(l,r)+W(l1,r+1)W(l,r+1)+W(l1,r)

可以得出 Wdp 满足四边形不等式,故而可以使用二维决策单调性优化转移,时间复杂度为 O(pv)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=3010;
const int maxp=310;
int p,v,i,j,k,n,b;
int a[maxn];
int w[maxn][maxn],dp[maxp][maxn],mp[maxp][maxn];
int main(){
scanf("%d%d",&v,&p);
for(i=1;i<=v;++i) scanf("%d",a+i);
for(i=1;i<=v;++i) for(j=i+1;j<=v;++j) w[i][j]=w[i][j-1]+a[j]-a[(i+j)>>1];
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(i=1;i<=p;++i){
mp[i][v+1]=v;
for(j=v;j;--j){
n=0x3f3f3f3f;
for(k=mp[i-1][j];k<=mp[i][j+1];++k){
if(dp[i-1][k]+w[k+1][j]<n){
n=dp[i-1][k]+w[k+1][j];
b=k;
}
}
mp[i][j]=b;
dp[i][j]=n;
}
}
printf("%d",dp[p][v]);
return 0;
}

序列变换 (CTSC 2009)

题意

给定一个长为 n 的数组 x,满足 i[1,n],1xiq,xixi+1,其中
aq1n1,aba,b,x 均为正整数。

现在对每个 xi 加上一个整数 di,使新序列 y 满足 i[i,n],1yiq,yi+1yi[a,b]

mini=1n|di|n5×105,q109

解法

代码

Jiry Matchings (Codeforces 300iq contest 2 J)

题意

有一棵大小为 n 的边带权树。对于 k[1,n),找出树上有 k 条边的最大带权匹配的边权之和。n2×105

解法

代码

Bear and Bowling (Codeforces CF573E)

题意

给定一个长为 n 的数组 aba 的一个子序列(可为空),令 b 长为 m,求 max(i=1mibi)n105

解法

朴素策略:有一个 O(n2) 的较为简单的 dp 。

dpi,j 表示i 个数选 j 个数组成子序列的最大答案,此时有

dpi,j=max(dpi1,j,dpi1,j1+aij)

一个猜想如下:iki[0,i],满足

j<ki, dpi,j=dpi1,j

jki, dpi,j=dpi1,j1+jai

考虑对于 dpi 构造一个差分序列 fi。若猜想成立,由上述 ki 的定义,有 dpi1,ki1dpi1,ki2+ai(ki1)dpi1,ki<dpi1,ki1+ai(ki1),对应 fi1,ki1ki1ai(ki1)fi1,kiki<ai

注意 ai 取值与 fi1 无关,所以 fi1,jj 关于 j 单调不增

计算 dpi 值后,可知

j<ki,fi,j=fi1,j

j>ki,fi,j=fi1,j1+ai

fi,ki=aiki

简单来讲,计算 dpi 后,fi 等效于在 fi1 中, 在 fi1,ki1 后插入 aiki,对 fi1,ki1 后整体加上 ai

考虑证明若猜想成立,则 fi,jj 关于 j 仍单调不增

对于 j<ki,显然 fi,j 不发生变化。

对于 ki,由于 j<ki,fi1,jj<aifi1,kiki=kiaiki=ai,单调性显然也没有改变。

对于 j>ki,有 fi1,jjfi1,j+1j+1,故而 fi1,jjj+1fi1,j+1

同时 fi,j+1=fi1,j+ai,fi,j+2=fi1,j+1+ai,考虑从结论倒推,可得

fi,j+1j+1>fi,j+2j+2fi1,j+aij+1>fi1,j+1+aij+2fi1,j+ai>j+1j+2(fi1,j+1+ai)fi1,j+ai>j+1j+2fi1,j+1+j+1j+2aifi1,j>j+1j+2fi1,j+11j+2ai

=(jj+1fi1,j+1)(j+1j+2fi1,j+11j+2ai)=((j+2)j(j+1)(j+1))fi1,j+1+(j+1)ai(j+1)(j+2)=(j+1)aifi1,j+1(j+1)(j+2)0

最后由 fi1 单调不增一定有 fi 单调不增。而 f1 只有一个数(单调不增),通过数学归纳法可得 fi 均是单调不增,从而证明猜想成立。

这里我使用的是 FHQ Treap 实现二分求 ki,单点插入,区间加。具体来讲,只维护 f 数组,在 FHQ Treap 的按权值分裂时,按 fi,jaij 的大小关系为二分依据。时间复杂度为 O(nlogn)

p.s. 感谢 plate_let 巨佬提供的思路!

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=100010;
int n,_x,_y,tot,root;
mt19937 Rand(time(0));
long long a,ans;
struct node{
int ls,rs,siz;
unsigned key;
ll val,add;
}tr[maxn];
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define siz(p) tr[p].siz
#define key(p) tr[p].key
#define val(p) tr[p].val
#define add(p) tr[p].add
inline int New(const ll &x){
val(++tot)=x;
key(tot)=Rand();
siz(tot)=1;
return tot;
}
inline void Pushdown(const int p){
if(!add(p)) return;
add(ls(p))+=add(p);
add(rs(p))+=add(p);
val(ls(p))+=add(p);
val(rs(p))+=add(p);
add(p)=0;
}
inline void Pushup(const int p){
siz(p)=siz(ls(p))+siz(rs(p))+1;
}
void vSplit(const int p,const int rnk,int &x,int &y){
if(!p){
x=y=0;
return;
}
Pushdown(p);
if(a*(rnk+siz(ls(p))+1)>val(p)){
y=p;
vSplit(ls(p),rnk,x,ls(p));
}
else{
x=p;
vSplit(rs(p),rnk+siz(ls(p))+1,rs(p),y);
}
Pushup(p);
}
int Merge(const int x,const int y){
if(!(x&&y)) return x|y;
if(key(x)<key(y)){
Pushdown(x);
rs(x)=Merge(rs(x),y);
Pushup(x);return x;
}
else{
Pushdown(y);
ls(y)=Merge(x,ls(y));
Pushup(y);return y;
}
}
void Auto(const int p){
Pushdown(p);
if(ls(p)) Auto(ls(p));
ans=max(ans,a+=val(p));
if(rs(p)) Auto(rs(p));
}
int main(){
scanf("%d",&n);
while(n--){
scanf("%lld",&a);
vSplit(root,0,_x,_y);
val(_y)+=a;add(_y)+=a;
root=Merge(Merge(_x,New(a*(siz(_x)+1))),_y);
}
a=0;Auto(root);
printf("%lld\n",ans);
return 0;
}
posted @   Fran-Cen  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示