【做题记录】dp(马思博)
A. 【模板】动态 DP
好像是我做过的最难的一道 DDP(?)
普通的转移 \(\begin{cases}f_{u,0}=\sum\max(f_{v,0},f_{v,1})\\f_{u,1}=a_u+\sum f_{v,0}\end{cases}\) 是不好优化的,我们希望 \(f_{u,0/1}\) 只从一个节点转移来。不妨设其中一个节点为 \(v\)(实际上就是重儿子,至于为什么一会儿再说),设 \(g_{u,0}\) 表示不选 \(u\),只考虑轻儿子的最大独立集,\(g_{u,1}\) 表示选 \(u\),只考虑轻儿子的最大独立集。于是有新转移 \(\begin{cases}f_{u,0}=g_{u,0}+\max(f_{v,0},f_{v,1})\\f_{u,1}=g_{u,1}+f_{v,0}\end{cases}\)。于是可以用线段树维护矩阵,由于从下往上转移,矩阵设计为左乘的形式比较方便。每次修改将 \(u\) 的根链所有链头的父亲的矩阵改变即可。这也就是重剖的原因,保证了时间复杂度线性对数方。
Code
#include<bits/stdc++.h>
#define int long long
#define il inline
#define pb push_back
#define lid id<<1
#define rid id<<1|1
using namespace std;
namespace asbt{
const int maxn=1e5+5,inf=1e18;
int n,m,a[maxn],sz[maxn],hes[maxn],fa[maxn];
int dfn[maxn],cnt,stk[maxn],top[maxn];
int bot[maxn],f[maxn][2];
vector<int> e[maxn];
struct juz{
int a[2][2];
juz(){
a[0][0]=a[0][1]=a[1][0]=a[1][1]=-inf;
}
il int*operator[](int x){
return a[x];
}
il juz operator*(juz x)const{
juz r;
for(int i:{0,1}){
for(int j:{0,1}){
r[i][j]=max(a[i][0]+x[0][j],a[i][1]+x[1][j]);
}
}
return r;
}
}b[maxn],tr[maxn<<2];
il void dfs1(int u){
sz[u]=1;
int mxs=0;
for(int v:e[u]){
if(v==fa[u]){
continue;
}
fa[v]=u;
dfs1(v);
sz[u]+=sz[v];
if(mxs<sz[v]){
mxs=sz[v],hes[u]=v;
}
}
}
il void dfs2(int u){
if(!top[u]){
top[u]=u;
}
dfn[u]=++cnt,stk[cnt]=u;
bot[top[u]]=u;
if(hes[u]){
top[hes[u]]=top[u];
dfs2(hes[u]);
}
int &g0=b[u][0][0]=0,&g1=b[u][1][0]=a[u];
for(int v:e[u]){
if(v==fa[u]||v==hes[u]){
continue;
}
dfs2(v);
g0+=max(f[v][0],f[v][1]);
g1+=f[v][0];
}
b[u][0][1]=g0;
f[u][0]=0,f[u][1]=a[u];
for(int v:e[u]){
if(v==fa[u]){
continue;
}
f[u][0]+=max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
}
}
il void pushup(int id){
tr[id]=tr[lid]*tr[rid];
}
il void build(int id,int l,int r){
if(l==r){
tr[id]=b[stk[l]];
return ;
}
int mid=(l+r)>>1;
build(lid,l,mid);
build(rid,mid+1,r);
pushup(id);
}
il void upd(int id,int l,int r,int p){
if(l==r){
tr[id]=b[stk[p]];
return ;
}
int mid=(l+r)>>1;
if(p<=mid){
upd(lid,l,mid,p);
}else{
upd(rid,mid+1,r,p);
}
pushup(id);
}
il juz query(int id,int L,int R,int l,int r){
if(L>=l&&R<=r){
return tr[id];
}
int mid=(L+R)>>1;
if(r<=mid){
return query(lid,L,mid,l,r);
}
if(l>mid){
return query(rid,mid+1,R,l,r);
}
return query(lid,L,mid,l,r)*query(rid,mid+1,R,l,r);
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
e[u].pb(v),e[v].pb(u);
}
dfs1(1),dfs2(1),build(1,1,n);
// for(int i=1;i<=n;i++){
// cout<<i<<' '<<sz[i]<<' '<<hes[i]<<' '<<dfn[i]<<' '<<top[i]<<' '<<bot[i]<<'\n';
// }
while(m--){
int u,x;
cin>>u>>x;
b[u][1][0]+=x-a[u],a[u]=x;
while(u){
juz p=query(1,1,n,dfn[top[u]],dfn[bot[top[u]]]);
upd(1,1,n,dfn[u]);
juz q=query(1,1,n,dfn[top[u]],dfn[bot[top[u]]]);
u=fa[top[u]];
int &g0=b[u][0][0],&g1=b[u][1][0];
g0-=max(p[0][0],p[1][0]);
g0+=max(q[0][0],q[1][0]);
g1-=p[0][0],g1+=q[0][0];
b[u][0][1]=g0;
}
juz r=query(1,1,n,dfn[1],dfn[bot[1]]);
cout<<max(r[0][0],r[1][0])<<'\n';
}
return 0;
}
}
signed main(){return asbt::main();}
C. [POI 2011] Lightning Conductor
相当于是对于每个 \(i\),求 \(\max\{a_j+\sqrt{i-j}-a_i\}\)。满足决策单调性,分治即可。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=5e5+5,inf=1e9;
int n,a[maxn],ans[maxn];
double sq[maxn];
il void solve(int L,int R,int l,int r){
if(l>r){
return ;
}
int mid=(l+r)>>1,p=0;
double v=-inf;
for(int i=L;i<=min(mid,R);i++){
double t=a[i]-a[mid]+sq[mid-i];
if(v<t){
p=i,v=t;
}
}
ans[mid]=max(ans[mid],(int)ceil(v));
solve(L,p,l,mid-1);
solve(p,R,mid+1,r);
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
sq[i]=sqrt(i);
}
memset(ans,-0x3f,sizeof(ans));
solve(1,n,1,n);
reverse(a+1,a+n+1);
reverse(ans+1,ans+n+1);
solve(1,n,1,n);
for(int i=n;i;i--){
cout<<max(ans[i],0)<<'\n';
}
return 0;
}
}
int main(){return asbt::main();}
D. [国家集训队] Tree I
我们设选了 \(i\) 条白边时的最小生成树为 \(f_i\),于是得到了若干个点 \((i,f_i)\)。打表可得,这些点组成了一个下凸包。我们考虑二分斜率 \(k\),找到切点 \((i,f_i)\),当 \(i=need\) 时 \(f_i\) 就是答案。如下图:

问题转变为求切点。容易发现,在所有斜率为 \(k\) 的直线中,切点所在的那一条纵截距最小。如下图:

那么我们怎么求最小的纵截距呢?设这条直线为 \(y=kx+b\),则有 \(kx+b=f_x\),于是纵截距即为 \(b=f_x-kx\),也就是给每条白边都减去了 \(k\)。于是我们给白边减去 \(k\) 再求最小生成树即可。时间复杂度线性对数。
实际上,这个算法叫做 wqs 二分。解决这种“钦定选 \(need\) 个物品时的最值”问题时经常使用 wqs 二分,它的特点是要求 \(f_i\) 必须是凸函数、细节多得像屎。至于本题中的二分上下界,因为凸包的最小/大斜率一定不超过 \([-100,100]\),因此定为 \([-100,100]\) 即可。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e5+5;
int n,m,kk,ca,cb,fa[maxn];
struct edge{
int u,v,w,opt;
il bool operator<(const edge &x)const{
return w<x.w;
}
}a[maxn],b[maxn],c[maxn];
il int find(int x){
return x!=fa[x]?fa[x]=find(fa[x]):x;
}
il bool check(int x){
int p1=1,p2=1,p3=1;
while(p1<=ca&&p2<=cb){
if(a[p1].w-x<=b[p2].w){
c[p3++]=a[p1++];
}
else{
c[p3++]=b[p2++];
}
}
while(p1<=ca){
c[p3++]=a[p1++];
}
while(p2<=cb){
c[p3++]=b[p2++];
}
for(int i=0;i<n;i++){
fa[i]=i;
}
int cnt=0;
for(int i=1;i<=m;i++){
int u=find(c[i].u),v=find(c[i].v);
if(u!=v){
fa[u]=v,cnt+=c[i].opt^1;
}
}
return cnt>=kk;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m>>kk;
for(int i=1,u,v,w,opt;i<=m;i++){
cin>>u>>v>>w>>opt;
if(!opt){
a[++ca]={u,v,w,opt};
}
else{
b[++cb]={u,v,w,opt};
}
}
sort(a+1,a+ca+1),sort(b+1,b+cb+1);
int l=-100,r=100;
while(l<r){
int mid=(l+r)>>1;
if(check(mid)){
r=mid;
}
else{
l=mid+1;
}
}
int p1=1,p2=1,p3=1;
while(p1<=ca&&p2<=cb){
if(a[p1].w-l<=b[p2].w){
c[p3]=a[p1++];
c[p3++].w-=l;
}
else{
c[p3++]=b[p2++];
}
}
while(p1<=ca){
c[p3]=a[p1++];
c[p3++].w-=l;
}
while(p2<=cb){
c[p3++]=b[p2++];
}
for(int i=0;i<n;i++){
fa[i]=i;
}
int ans=0;
for(int i=1;i<=m;i++){
int u=find(c[i].u),v=find(c[i].v);
if(u!=v){
fa[u]=v,ans+=c[i].w;
}
}
cout<<ans+kk*l;
return 0;
}
}
int main(){return asbt::main();}
E. Gosha is hunting
显然有三维的 DP:设 \(f_{i,j,k}\) 表示考虑到第 \(i\) 个神奇宝贝,用了 \(j\) 个宝贝球和 \(k\) 个超级球的最大期望。可以发现 \(f_{i,j,k}\) 是关于 \(k\) 的上凸包,wqs 二分即可。时间复杂度 \(O(n^2\log V)\)。
LG 上的这篇讨论中有相当多的 hack 数据,非常毒瘤的是那组号称更小但更强的数据是错的(害得我干瞪着代码看了十五分钟。。。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=2e3+5;
const double eps=1e-8;
int n,a,b,g[maxn][maxn];
double p[maxn],q[maxn];
double f[maxn][maxn];
il bool check(double x){
for(int i=1;i<=n;i++){
for(int j=0;j<=a;j++){
f[i][j]=g[i][j]=0;
if(f[i][j]<f[i-1][j]){
f[i][j]=f[i-1][j];
g[i][j]=g[i-1][j];
}
if(j&&f[i][j]<f[i-1][j-1]+p[i]){
f[i][j]=f[i-1][j-1]+p[i];
g[i][j]=g[i-1][j-1];
}
if(f[i][j]<f[i-1][j]+q[i]-x){
f[i][j]=f[i-1][j]+q[i]-x;
g[i][j]=g[i-1][j]+1;
}
if(j&&f[i][j]<f[i-1][j-1]+p[i]+q[i]-x-p[i]*q[i]){
f[i][j]=f[i-1][j-1]+p[i]+q[i]-x-p[i]*q[i];
g[i][j]=g[i-1][j-1]+1;
}
}
}
return g[n][a]<=b;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>a>>b;
for(int i=1;i<=n;i++){
cin>>p[i];
}
for(int i=1;i<=n;i++){
cin>>q[i];
}
double l=0,r=1;
while(r-l>eps){
double mid=(l+r)/2;
if(check(mid)){
r=mid;
}else{
l=mid;
}
}
check(l);
// cout<<l<<' '<<g[n][a]<<'\n';
cout<<fixed<<setprecision(6)<<f[n][a]+l*b;
return 0;
}
}
int main(){return asbt::main();}
H. 小N的独立集
最大独立集有一个经典 DP:设 \(dp_{i,0/1}\) 表示选/不选 \(i\),\(i\) 的子树的最大独立集。因为要求满足 \(dp_{i,0/1}=x\) 的树的数量, 所以考虑 DP 套 DP:设 \(f_{i,j,k}\) 表示 \(dp_{i,0}=j,dp_{i,1}=k\) 的方案数。但这样时间空间都是开不下的,考虑修改 DP 数组的定义,\(dp_{i,0/1}\) 表示强制/不强制不选 \(i\) 的最大独立集,\(f_{i,j,k}\) 表示 \(dp_{i,0}=j,dp_{i,1}=j+k\) 的方案数,因为 \(k\le5\) 所以直接树上背包转移即可。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define pb push_back
using namespace std;
namespace asbt{
const int maxn=1e3+5,mod=1e9+7;
int n,m,sz[maxn];
ll f[maxn][maxn*5][7],g[maxn*5][7];
vector<int> e[maxn];
il void dfs(int u,int fa){
sz[u]=1;
for(int i=1;i<=m;i++){
f[u][0][i]=1;
}
for(int v:e[u]){
if(v==fa){
continue;
}
dfs(v,u);
for(int i=0;i<=(sz[u]+sz[v])*m;i++){
for(int j=0;j<=m;j++){
g[i][j]=0;
}
}
for(int i1=0;i1<=sz[u]*m;i1++){
for(int j1=0;j1<=m;j1++){
if(!f[u][i1][j1]){
continue;
}
for(int i2=0;i2<=sz[v]*m;i2++){
for(int j2=0;j2<=m;j2++){
if(!f[v][i2][j2]){
continue;
}
(g[i1+i2+j2][max(j1-j2,0)]+=f[u][i1][j1]*f[v][i2][j2])%=mod;
}
}
}
}
sz[u]+=sz[v];
for(int i=0;i<=sz[u]*m;i++){
for(int j=0;j<=m;j++){
f[u][i][j]=g[i][j];
}
}
}
}
int main(){
freopen("nset.out","w",stdout);
freopen("nset.in","r",stdin);
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
e[u].pb(v),e[v].pb(u);
}
dfs(1,0);
for(int i=1;i<=n*m;i++){
ll ans=0;
for(int j=0;j<=min(i,m);j++){
ans+=f[1][i-j][j];
}
cout<<ans%mod<<"\n";
}
return 0;
}
}
int main(){return asbt::main();}
I. [NOI Online #1 入门组] 跑步
问题等价于划分成若干无序的数,\(O(n^2)\) 的多重背包是显然的。
考虑根号分治,对于划分中 \(<\sqrt{n}\) 的数,用多重背包求;对于 \(\ge\sqrt{n}\) 的数,设有 \(i\) 个,和为 \(j\) 的方案数为 \(g_{j,i}\),于是有 \(g_{j,i}=g_{j-i,i}+g_{j-m,i-1}\),相当于是加入一个 \(m\) 或给每个数加 \(1\)。最后统计答案即可,时间复杂度 \(O(n\sqrt{n})\)。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e5+5;
int n,m,mod,f[maxn],g[maxn][325];
il int pls(int x,int y){
return x+y<mod?x+y:x+y-mod;
}
il int mns(int x,int y){
return x<y?x-y+mod:x-y;
}
il void add(int &x,int y){
x=pls(x,y);
}
il void sub(int &x,int y){
x=mns(x,y);
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>mod;
m=sqrt(n)+1;
f[0]=1;
for(int i=1;i<m;i++){
for(int j=i;j<=n;j++){
add(f[j],f[j-i]);
}
}
g[0][0]=1;
for(int i=1;i<m;i++){
for(int j=i;j<=n;j++){
g[j][i]=g[j-i][i];
if(j>=m){
add(g[j][i],g[j-m][i-1]);
}
}
}
int ans=0;
for(int i=0;i<=n;i++){
int sum=0;
for(int j=0;j<m;j++){
add(sum,g[i][j]);
}
ans=(f[n-i]*1ll*sum+ans)%mod;
}
cout<<ans;
return 0;
}
}
int main(){return asbt::main();}
J. [agc001_e]BBQ Hard
\(a_i+b_i+a_j+b_j\choose a_i+b_i\) 其实就等价于从 \((-a_i,-b_i)\) 走到 \((a_j,b_j)\) 的方案数,于是 \(O(n^2)\) DP 即可。需要减去 \(i\) 自己给自己的贡献,还有 \(i>j\) 的贡献。
Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=2e5+5,maxm=4e3+5,mod=1e9+7,inv2=5e8+4;
int n,a[maxn],b[maxn],fac[maxm<<1],inv[maxm<<1],f[maxm][maxm];
il int qpow(int x,int y=mod-2){
int res=1;
while(y){
if(y&1){
(res*=x)%=mod;
}
(x*=x)%=mod,y>>=1;
}
return res;
}
il void init(int n=8e3){
fac[0]=1;
for(int i=1;i<=n;i++){
fac[i]=fac[i-1]*i%mod;
}
inv[n]=qpow(fac[n]);
for(int i=n;i;i--){
inv[i-1]=inv[i]*i%mod;
}
}
il int C(int x,int y){
if(x<y||y<0){
return 0;
}
return fac[x]*inv[y]%mod*inv[x-y]%mod;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n;
init();
int ans=0;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i];
(ans+=mod-C((a[i]+b[i])<<1,a[i]<<1))%=mod;
// cout<<C((a[i]+b[i])<<1,a[i]<<1)<<"\n";
f[2001-a[i]][2001-b[i]]++;
}
for(int i=1;i<=4001;i++){
for(int j=1;j<=4001;j++){
f[i][j]=(f[i][j]+f[i-1][j]+f[i][j-1])%mod;
}
}
for(int i=1;i<=n;i++){
(ans+=f[2001+a[i]][2001+b[i]])%=mod;
// cout<<f[2001+a[i]][2001+b[i]]<<"\n";
}
cout<<ans*inv2%mod;
return 0;
}
}
signed main(){return asbt::main();}
M. [CEOI 2016] kangaroo
问题可以转化为求 \(p_1=s\),\(p_n=t\) 且 \(p_i\) 两边同时大于/小于 \(p_i\) 的排列 \(p\) 的个数。
考虑连续段 DP,设 \(f_{i,j}\) 表示考虑前 \(i\) 个数,分成了 \(j\) 段的方案数。转移分两种:
-
新开一段,\(f_{i,j}\leftarrow(j-[i>s]-[i>t])\times f_{i-1,j-1}\)
-
将两段连起来,\(f_{i,j}\leftarrow j\times f_{i-1,j+1}\)
注意特判 \(i=s\) 或 \(t\) 的情况。时间复杂度 \(O(n^2)\)。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=2e3+5,mod=1e9+7;
int n,s,t,f[maxn][maxn];
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>s>>t;
f[1][1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
if(i==s||i==t){
f[i][j]=(f[i-1][j-1]+f[i-1][j])%mod;
}
else{
f[i][j]=(f[i-1][j-1]*1ll*(j-(i>s)-(i>t))+f[i-1][j+1]*1ll*j)%mod;
}
}
}
cout<<f[n][1];
return 0;
}
}
int main(){return asbt::main();}
N. [CSP-S 2022] 数据传输
对 \(K\) 分讨。
\(K=1\),直接倍增求和即可。
\(K=2\),考虑将路径拉出来 DP。设 \(f_i\) 表示走到第 \(i\) 个点的最小花费。故有 \(f_i=\min(f_{i-1},f_{i-2})+a_i\)。考虑 DDP,可以构造出转移矩阵:
\(K=3\),此时我们发现可以走到路径上某个点的某个相邻点来减小花费。如下图:

可以发现每个点挂的点要么是相邻的最小值,要么就不挂点。记这个挂的点为 \(b_i\)。类似地仍然考虑 DP。设 \(f_{i,0/1}\) 表示走到 \(i\)/\(i\) 挂的点所需的最小花费。于是有转移:
但是这个东西如果用矩阵优化,代码会带上 \(125\) 的常数。考虑换个定义。发现 DP 转移只和到 \(i\) 的距离有关,所以设 \(f_{i,0/1/2}\) 表示走到到 \(i\) 的距离为 \(0/1/2\) 的点的最小花费,于是有转移:
进而可以设计出转移矩阵:
于是倍增即可,时间复杂度 \(O(n\log n)\)。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define pb push_back
using namespace std;
namespace asbt{
const int maxn=2e5+5;
const ll inf=1e18;
int n,m,kk,anc[maxn][18],dep[maxn];
ll a[maxn],b[maxn];
vector<int> e[maxn];
struct juz{
ll mat[4][4];
juz(bool x=false){
for(int i=1;i<=kk;i++){
for(int j=1;j<=kk;j++){
mat[i][j]=inf;
}
}
if(x){
for(int i=1;i<=kk;i++){
mat[i][i]=0;
}
}
}
juz(int x){
switch(kk){
case 1:{
mat[1][1]=a[x];
break;
}
case 2:{
mat[1][1]=mat[2][1]=a[x];
mat[1][2]=0,mat[2][2]=inf;
break;
}
default:{
mat[1][1]=mat[2][1]=mat[3][1]=a[x];
mat[1][2]=mat[2][3]=0;
mat[1][3]=mat[3][3]=inf;
mat[2][2]=b[x],mat[3][2]=a[x]+b[x];
break;
}
}
}
il ll*operator[](int x){
return mat[x];
}
il juz operator*(juz x)const{
juz res;
for(int i=1;i<=kk;i++){
for(int j=1;j<=kk;j++){
for(int k=1;k<=kk;k++){
res[i][j]=min(res[i][j],mat[i][k]+x[k][j]);
}
}
}
return res;
}
}up[maxn][18],dw[maxn][18];
il void dfs1(int u,int fa){
b[u]=inf;
for(int v:e[u]){
b[u]=min(b[u],a[v]);
if(v==fa){
continue;
}
dfs1(v,u);
}
}
il void dfs2(int u,int fa){
anc[u][0]=fa,dep[u]=dep[fa]+1;
up[u][0]=dw[u][0]=juz(u);
for(int i=1;i<=17;i++){
anc[u][i]=anc[anc[u][i-1]][i-1];
up[u][i]=up[u][i-1]*up[anc[u][i-1]][i-1];
dw[u][i]=dw[anc[u][i-1]][i-1]*dw[u][i-1];
}
for(int v:e[u]){
if(v==fa){
continue;
}
dfs2(v,u);
}
}
il juz dp(int u,int v){
juz resu,resv(true);
if(dep[u]==dep[v]){
resv=dw[v][0],v=anc[v][0];
}
else if(dep[u]<dep[v]){
swap(u,v);
}
resu[1][1]=a[u],u=anc[u][0];
int ddep=dep[u]-dep[v],d=0;
// if(!m){
// cout<<"kpo "<<ddep<<"\n";
// }
while(ddep){
if(ddep&1){
resu=resu*up[u][d];
u=anc[u][d];
}
ddep>>=1,d++;
}
if(u==v){
return resu*up[u][0]*resv;
}
for(int i=17;~i;i--){
if(anc[u][i]!=anc[v][i]){
resu=resu*up[u][i];
resv=dw[v][i]*resv;
u=anc[u][i],v=anc[v][i];
}
}
return resu*up[u][1]*dw[v][0]*resv;
}
int main(){
// freopen("P8820_1.in","r",stdin);
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m>>kk;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
e[u].pb(v),e[v].pb(u);
}
dfs1(1,0),dfs2(1,0);
while(m--){
int u,v;
cin>>u>>v;
cout<<dp(u,v)[1][1]<<"\n";
}
return 0;
}
}
int main(){return asbt::main();}
O. [NOI2009] 诗人小G
设 \(dp_i\) 表示考虑到 \(i\) 的最小不协调度,则有方程:
后面那个东西满足四边形不等式,因此可以决策单调性优化。维护决策点队列,二分出每个决策点可以作为最优决策点的范围即可转移。时间复杂度线性对数。需要 long double。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define ld long double
using namespace std;
namespace asbt{
const int maxn=1e5+5;
const ld inf=1e18;
int T,n,m,kk,a[maxn],pre[maxn],q[maxn];
ld dp[maxn];
string s[maxn];
il ld qpow(ld x,int y){
ld res=1;
while(y){
if(y&1){
res*=x;
}
x*=x,y>>=1;
}
return res;
}
il ld calc(int x,int y){
return dp[x]+qpow(abs(a[y]-a[x]+y-x-1-m),kk);
}
il int find(int x,int y){
int l=y,r=n+1;
while(l<r){
int mid=(l+r)>>1;
if(calc(x,mid)>=calc(y,mid)){
r=mid;
}
else{
l=mid+1;
}
}
return l;
}
il void print(int x){
if(!x){
return ;
}
print(pre[x]);
for(int i=pre[x]+1;i<x;i++){
cout<<s[i]<<" ";
}
cout<<s[x]<<"\n";
}
il void solve(){
cin>>n>>m>>kk;
for(int i=1;i<=n;i++){
cin>>s[i];
a[i]=a[i-1]+s[i].size();
dp[i]=inf,pre[i]=0;
}
int hd=1,tl=0;
dp[0]=0,q[++tl]=0;
for(int i=1;i<=n;i++){
while(hd<tl&&find(q[hd],q[hd+1])<=i){
hd++;
}
dp[i]=calc(q[hd],i),pre[i]=q[hd];
while(hd<tl&&find(q[tl-1],q[tl])>=find(q[tl],i)){
tl--;
}
q[++tl]=i;
}
if(dp[n]>inf){
cout<<"Too hard to arrange\n";
}
else{
cout<<(ll)dp[n]<<"\n";
print(n);
}
cout<<"--------------------\n";
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>T;
while(T--){
solve();
}
return 0;
}
}
int main(){return asbt::main();}
P. 忘情
设 \(dp_{i,j}\) 表示到 \(i\) 分了 \(j\) 段的最小值,于是有转移:
发现 \(dp_{i,j}\) 关于 \(j\) 是一个下凸函数,于是可以 wqs 二分。注意为了使截距更小,在截到 \(m\) 点时要继续让斜率变大。
于是内层 DP 是这样一个东西:设 \(f_i\) 为考虑到 \(i\) 的答案,有 \(f_i=\min\limits_{j=0}^{i-1}\{f_j+(sum_i-sum_j+1)^2-k\}\)。化简得 \(f_j+s_j^2-2s_j=2s_is_j+f_i-s_i^2-2s_i-1+k\)。于是有 \(k_i=2s_i\),\(b_i=f_i-s_i^2-2s_i-1+k\),\(x_j=s_j\),\(y_j=f_j+s_j^2-2s_j\)。因为是最小值,我们用单调队列维护 \(j\) 组成的下凸包,用斜率为 \(2s_i\) 的直线去切这个下凸包,斜率优化 DP 即可。
Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e5+5;
int n,m,a[maxn],f[maxn],g[maxn],q[maxn];
il bool check(int x){
int hd=1,tl=0;
f[0]=0,q[++tl]=0;
#define X(i) (a[i])
#define Y(i) (f[i]+a[i]*a[i]-2*a[i])
#define K(i,j) ((Y(j)-Y(i))*1.0l/(X(j)-X(i)))
for(int i=1;i<=n;i++){
while(hd<tl&&K(q[hd],q[hd+1])<2*a[i]){
hd++;
}
f[i]=f[q[hd]]+(a[i]-a[q[hd]]+1)*(a[i]-a[q[hd]]+1)-x;
g[i]=g[q[hd]]+1;
while(hd<tl&&K(q[tl-1],q[tl])>K(q[tl],i)){
tl--;
}
q[++tl]=i;
}
#undef X
#undef Y
#undef K
return g[n]<=m;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
a[i]+=a[i-1];
}
int l=-2e16,r=0;
while(l<r){
int mid=(l+r+1)>>1;
if(check(mid)){
l=mid;
}
else{
r=mid-1;
}
}
check(l);
cout<<f[n]+m*l;
return 0;
}
}
signed main(){return asbt::main();}
Q. [ICPC 2018 WF] Gem Island
首先考虑每一种情况的概率。设最后每个人的宝石数为 \(c_i\),于是方案数为 \(\prod\limits_{i=1}^{n}{d-\sum_{j=1}^{i-1}(c_j-1)\choose(c_i-1)}\times\prod\limits_{i=1}^{n}(c_i-1)!=d!\)。于是每种情况的概率都是一样的,我们只需 DP 出方案数和总和即可。
先不考虑最开始那 \(n\) 个宝石。设 \(f_{i,j}\) 表示当前共有 \(i\) 个宝石,拥有最多宝石的人有 \(j\) 个时的方案数,于是有转移方程:
类似地,设 \(g_{i,j}\) 表示此时前 \(r\) 大的总和,于是有方程:
答案即为 \(\frac{\sum_{i=1}^{n}f_{d,i}}{\sum_{i=1}^{n}g_{d,i}}\)。时间复杂度 \(O(n^3)\)。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e3+5;
int n,d,r;
double f[maxn][maxn],g[maxn][maxn],C[maxn][maxn];;
il void init(int n=1e3){
C[0][0]=1;
for(int i=1;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
C[i][j]=C[i-1][j-1]+C[i-1][j];
}
}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>d>>r;
init();
f[0][n]=1;
for(int i=0;i<=d;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=j;k++){
f[i+k][k]+=f[i][j]*C[j][k];
g[i+k][k]+=(g[i][j]+min(k,r)*f[i][j])*C[j][k];
}
}
}
double sg=0,sf=0;
for(int i=1;i<=n;i++){
sg+=g[d][i],sf+=f[d][i];
}
cout<<fixed<<setprecision(7)<<sg/sf+r;
return 0;
}
}
int main(){return asbt::main();}
S. [JOI Open 2016] 摩天大楼 / Skyscraper
首先将 \(a\) 排序,然后可以连续段 DP,设 \(f_{i,j,k,d}\) 表示前 \(i\) 个数连成 \(j\) 段,和为 \(k\),当前确定了 \(d\) 个边界的方案数。
一个比较直观的费用提前计算是,在插入每个数时预先算出比它大的数放在它旁边时它自己的贡献。举个例子,对于另成一段而不贴边界的转移,有:
其他转移是类似的。这样的时空复杂度是无法接受的,因为转移过程中 \(k\) 有增有减,虽然最后我们统计答案时只需要 \(k\in[0,L]\) 的贡献,转移过程中确是无法舍去一些 \(k\) 较大的状态的。我们希望换一种贡献的计算方法,使得转移过程中 \(k\) 只增不减。
考虑排序后的 \(a\) 数组中的相邻两项 \(a_i\) 和 \(a_{i+1}\) 的贡献,它们的贡献即为 \((a_{i+1}-a_i)\times cnt_i\),其中 \(cnt_i\) 为覆盖了 \(a_i\) 到 \(a_{i+1}\) 这一段的最终在题目所求排列里相邻的数对数量。考虑插入 \(a_{i+1}\),此时会对 \(cnt_i\) 产生贡献。因为插入 \(a_{i+1}\) 之前每一段的两端的数都 \(<a_{i+1}\),后面插入的数都 \(>a_{i+1}\),所以会产生的贡献就是 \(cnt_i=2j-d\)。于是新的贡献和 \(k'=k+(a_{i+1}-a_i)(2j-d)\)。于是就可以 DP 了,时间复杂度 \(O(n^2L)\)。
Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int mod=1e9+7;
int n,m,a[105],f[105][105][1005][3];
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
if(n==1){
cout<<1;
return 0;
}
sort(a+1,a+n+1);
f[0][0][0][0]=1;
for(int i=0;i<n;i++){
for(int j=0;j<=i;j++){
for(int k=0;k<=m;k++){
for(int d=0;d<=2;d++){
int p=k+(a[i+1]-a[i])*(2*j-d),t=f[i][j][k][d];
if(!t||p>m){
continue;
}
f[i+1][j+1][p][d]=(f[i+1][j+1][p][d]+t*1ll*(j+1-d))%mod;
if(d<2){
f[i+1][j+1][p][d+1]=(f[i+1][j+1][p][d+1]+t*1ll*(2-d))%mod;
}
if(j){
f[i+1][j][p][d]=(f[i+1][j][p][d]+t*1ll*(2*j-d))%mod;
}
if(j&&d<2){
f[i+1][j][p][d+1]=(f[i+1][j][p][d+1]+t*1ll*(2-d))%mod;
}
if(j>=2){
f[i+1][j-1][p][d]=(f[i+1][j-1][p][d]+t*1ll*(j-1))%mod;
}
}
}
}
}
int ans=0;
for(int i=0;i<=m;i++){
(ans+=f[n][1][i][2])%=mod;
}
cout<<ans;
return 0;
}
}
int main(){return asbt::main();}

浙公网安备 33010602011771号