Atcoder DP Contest 题目全讲(下)
前言:
本篇文章为下期(收录题目 \(\text{O-->Z}\)),题目比较有思维难度,值得一做。
O题
观察题目数据范围,允许状态压缩。
设 \(f_{i,s}\) 表示右部中的点,与左部中 \(i\) 个点构成匹配的集合为 \(s\) 的方案数,答案为 \(f_{n,(1<<n)-1}\)。
考虑如何转移,当构成匹配集合为 \(s\) 时,我们发现二进制表示下 \(1\) 的数量(即当前匹配数),应当等于 \(i\),这样才能更新。
找到一个右部的一个点,满足它不属于集合 \(s\),并且能与当前枚举的左部点匹配,这样就可以进行更新。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=22,M=(1<<N),mod=1e9+7;
int n,a[N][N],f[M];
int popcount(int x){int res=0;while(x){if(x&1)res++;x>>=1;}return res;}
int main(){
n=rd;FOR(i,0,n-1) FOR(j,0,n-1) a[i][j]=rd;
f[0]=1;
FOR(s,0,(1<<n)-1){
int cnt=popcount(s);
FOR(i,0,n-1) if(a[cnt][i]&&(s&(1<<i))==0) f[s|(1<<i)]=(1ll*f[s|(1<<i)]+f[s])%mod;
}
printf("%d\n",f[(1<<n)-1]);
return 0;
}
P题
基础树形 \(\text{dp}\)。
设 \(f_{x,0/1}\) 表示以 \(x\) 为根的子树中,\(x\) 号节点染白\(/\)黑色的方案数。
设边 \((x,y)\),为满足相邻两点不全为黑色的限制,转移为:
- \(f_{x,0}=f_{x,0}\times(f_{y,0}+f_{y,1})\)。
- \(f_{x,1}=f_{x,1}\times f_{y,0}\)。
设 \(1\) 为根,答案为 \(f_{1,0}+f_{1,1}\)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=1e5+10,mod=1e9+7;
int n,head[N],tot,f[N][2];
struct node{int to,nxt;}edge[N<<1];
void add(int x,int y){edge[++tot].to=y,edge[tot].nxt=head[x],head[x]=tot;}
void dfs(int x,int fa){
f[x][0]=f[x][1]=1;
for(int i=head[x];i;i=edge[i].nxt){
int y=edge[i].to;if(y==fa) continue;
dfs(y,x);
f[x][0]=1ll*f[x][0]*(f[y][0]+f[y][1])%mod;
f[x][1]=1ll*f[x][1]*f[y][0]%mod;
}
}
int main(){
n=rd;
FOR(i,1,n-1){int x=rd,y=rd;add(x,y),add(y,x);}
dfs(1,0);
printf("%d\n",(1ll*f[1][0]+f[1][1])%mod);
return 0;
}
Q题
状态设计和转移方程是很显然的,设 \(f_{i}\) 表示前 \(i\) 朵花,选了第 \(i\) 朵花能得到的最大权值。
转移为 \(f_{i}=\max_{j=0}^{i-1}\{f_j\}+a_i,h_j<h_i\)。
暴力做是 \(O(n^2)\) 的,考虑优化。
我们发现每次查找的是前面所有满足 \(h_j<h_i\) 的 \(f_j\) 的最大值,所以可以以 \(h\) 作为下标,建立权值线段树,维护区间中 \(f\) 最大值。每次查找的都是一段前缀,计算完后还要更新 \(f_i\)。
时间复杂度 \(O(nlogn)\)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=2e5+10;
int n,h[N],a[N];ll mx[N<<2];
void pushup(int u){mx[u]=max(mx[u<<1],mx[u<<1|1]);}
void modify(int u,int l,int r,int x,ll v){
if(l==r){mx[u]=v;return;}
int mid=(l+r)>>1;
if(x<=mid) modify(u<<1,l,mid,x,v);
else modify(u<<1|1,mid+1,r,x,v);
pushup(u);
}
ll query(int u,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return mx[u];
int mid=(l+r)>>1;ll ans=0;
if(ql<=mid) ans=max(ans,query(u<<1,l,mid,ql,qr));
if(qr>mid) ans=max(ans,query(u<<1|1,mid+1,r,ql,qr));
return ans;
}
int main(){
n=rd;ll ans=0;
FOR(i,1,n) h[i]=rd;
FOR(i,1,n) a[i]=rd;
FOR(i,1,n){
ll tmp=query(1,1,n,1,h[i])+a[i];
ans=max(ans,tmp),modify(1,1,n,h[i],tmp);
}
printf("%lld\n",ans);
return 0;
}
R题
矩阵加速 \(\text{dp}\)。
比较 \(\text{naive}\) 的 \(\text{dp}\) 很好想,设 \(f_{t,i,j}\) 表示从 \(i\to j\),经过路径长度为 \(t\) 的方案数,我们可以枚举中间某个点 \(k\),则 \(f_{t,i,j}=\sum_{k=1}^{n}f_{t-1,i,k}\times f_{1,k,j}\)。
我们发现这和矩阵乘法的式子一模一样,而 \(f_1\) 矩阵即为题中所给,而 \(f_t=f_{t-1}\times f_1\),所以最终的答案矩阵为 \(f_k=f_1^k\)。
最后就是一个矩阵加速幂的模版了,不会的可以学一学。
时间复杂度 \(O(n^3logk)\)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=55,mod=1e9+7;
int n;ll K;
struct matrix{
int a[N][N];
matrix(){memset(a,0,sizeof(a));}
matrix operator*(const matrix&T){
matrix res;
FOR(i,0,n-1){
FOR(k,0,n-1){
int r=a[i][k];
FOR(j,0,n-1)
res.a[i][j]=(1ll*res.a[i][j]+1ll*r*T.a[k][j]%mod)%mod;
}
}
return res;
}
}M,ans;
int main(){
n=rd,scanf("%lld",&K);
FOR(i,0,n-1) ans.a[i][i]=1;
FOR(i,0,n-1) FOR(j,0,n-1) M.a[i][j]=rd;
while(K){
if(K&1) ans=ans*M;
M=M*M,K>>=1;
}
int res=0;
FOR(i,0,n-1) FOR(j,0,n-1) res=(1ll*res+ans.a[i][j])%mod;
cout<<res<<endl;
return 0;
}
S题
蛮套路的数位 \(\text{dp}\)。
我们考虑记忆化搜索来写,从高位到低位枚举,记录当前所有数码和模 \(D\) 的值,注意判断是否有前导零。
然后写法就和其他记搜数位 \(\text{dp}\) 一样,看代码就能理解了。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=10010,M=110,mod=1e9+7;
int n,cnt,f[N][M][2],d,num[N];char ch[N];
int dfs(int pos,int sum,bool zero,bool lim){
if(!pos) return sum==0&&!zero;//枚举完了
if(!lim&&~f[pos][sum][zero]) return f[pos][sum][zero];//记搜
int up=lim?num[pos]:9,res=0;//是否有最高位的限制
FOR(i,0,up) res=(1ll*res+dfs(pos-1,(sum+i)%d,zero&&i==0,lim&&i==up))%mod;
if(!lim) f[pos][sum][zero]=res;
return res;
}
int main(){
scanf("%s",ch+1),n=strlen(ch+1),d=rd;
ROF(i,n,1) num[++cnt]=ch[i]-'0';
memset(f,-1,sizeof(f)),printf("%d\n",dfs(cnt,0,1,1));
return 0;
}
T题
这个题非常的妙!
一开始很容易想到 \(f_{i,j}\) 表示前 \(i\) 个数,第 \(i\) 位填 \(j\) 的方案数,然后 \(j\) 从 \(1\) 到 \(n\) 枚举,你会发现答案错了。为什么?因为你很容易把不合法的方案给统计上,会算重。
那么怎么做,我们发现其实只关心大小关系,比如 \(4,5,6\),完全可以映射为 \(1,2,3\),所以我们规定前 \(i\) 个数中,就只从 \(1\to i\) 中添数。所以 \(j\) 从 \(1\to i\) 枚举,对于转移:
- \(f_{i,j}=\sum_{k=1}^{j-1}f_{i-1,k},\text{(if op=<)}\)
- \(f_{i,j}=\sum_{k=j}^{i-1}f_{i-1,k},\text{(if op=>)}\)
你可能会疑惑第二个式子,因为对于 \(>\),我们可以将 \([1,i-1]\) 中 \(>j\) 的数都加上 \(1\),这样能保证值域为 \([1,i]\),且大小关系不变,并且可以从 \(i-1\) 转移。
转移可以用前缀和优化,时间复杂度 \(O(n^2)\)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=3010,mod=1e9+7;
int n,f[N][N],s[N][N];char ch[N];
int main(){
n=rd,scanf("%s",ch+1);
f[1][1]=s[1][1]=1;
FOR(i,2,n) FOR(j,1,i){
if(ch[i-1]=='<') f[i][j]=s[i-1][j-1];
else f[i][j]=(1ll*s[i-1][i-1]-s[i-1][j-1]+mod)%mod;
s[i][j]=(1ll*s[i][j-1]+f[i][j])%mod;
}
printf("%d\n",s[n][n]);
return 0;
}
U题
设 \(f_S\) 表示当前所选物品集合为 \(S\) 时所得的最大价值,转移为 \(f_S=\max \{\ f_{S'}+w_{S\oplus S'}\}\)(\(\oplus\) 为异或),枚举 \(S'\subseteq S\) 即可完成转移。
\(w_S\) 为将集合 \(S\) 中的数放入同一组中的价值,这个可以 \(O(2^nn^2)\) 预处理,然后转移 \(f\) 时枚举子集,最终复杂度为 \(O(2^nn^2+3^n)\)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=16,M=(1<<N)+10;
int n,a[N][N];ll f[M],w[M];
int main(){
n=rd;
FOR(i,0,n-1) FOR(j,0,n-1) a[i][j]=rd;
FOR(i,0,(1<<n)-1){
ll res=0;
FOR(j,0,n-1) FOR(k,j+1,n-1){
if(((i>>j)&1)&&((i>>k)&1)) res+=a[j][k];
}
f[i]=w[i]=res;
}
FOR(i,0,(1<<n)-1){
for(int s=i;s;s=(s-1)&i) f[i]=max(f[i],f[i^s]+w[s]);
}
printf("%lld\n",f[(1<<n)-1]);
return 0;
}
V题
树形 \(\text{dp}\) 好题。
要求所有节点的答案,考虑换根 \(\text{dp}\),先想根节点确定的做法。
设 \(f_x\) 表示以 \(x\) 为根的子树中,\(x\) 染成黑色所得到的连通块的方案数。
设 \(y\) 为 \(x\) 的儿子,转移为 \(f_x\gets f_x\times (f_y+1)\),表示 \(y\) 可以染黑色或白色。
再考虑换根 \(\text{dp}\),设 \(g_x\) 表示以 \(x\) 为根节点的子树外,染成黑色的节点与 \(x\) 构成一个连通块的方案数,考虑 \(fa\) 与它的兄弟的贡献:\(g_x=g_{fa}\prod_{v}(f_v+1)+1,v\in \text{brother(x)}\)。
暴力求是 \(O(n^2)\),可以在第一遍 \(\text{dfs}\) 时求出 \(f\) 的前缀积、后缀积,就可以做到 \(O(n)\) 了。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
#define debug cout<<"I love CCF"<<endl;
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=1e5+10;
int n,mod,head[N],tot,f[N],g[N],pre[N],nxt[N];
struct node{int to,nxt;}edge[N<<1];
void add(int x,int y){edge[++tot]={y,head[x]},head[x]=tot;}
void dfs(int x,int fa){
f[x]=1;vector<int> v;
for(int i=head[x];i;i=edge[i].nxt){
int y=edge[i].to;if(y==fa) continue;
dfs(y,x);
f[x]=1ll*f[x]*(f[y]+1)%mod;
v.push_back(y);
}
int res=1;
if(!v.size()) return;
for(int i=0;i<=v.size()-1;i++){
pre[v[i]]=res,res=1ll*res*(f[v[i]]+1)%mod;
}
res=1;
for(int i=v.size()-1;i>=0;i--){
nxt[v[i]]=res,res=1ll*res*(f[v[i]]+1)%mod;
}
}
void DFS(int x,int fa){
if(fa==0) g[x]=1;
else g[x]=(1ll*g[fa]*pre[x]%mod*nxt[x]%mod+1)%mod;
for(int i=head[x];i;i=edge[i].nxt){
int y=edge[i].to;if(y==fa) continue;
DFS(y,x);
}
}
int main(){
n=rd,mod=rd;
FOR(i,1,n-1){int x=rd,y=rd;add(x,y),add(y,x);}
dfs(1,0),DFS(1,0);
FOR(i,1,n){
int ans=1ll*f[i]*g[i]%mod;
printf("%d\n",ans);
}
return 0;
}
W题
区间贡献,直接做是很难转移的,所以把区间的右端点记下来,我们只在到达右端点时才更新这段区间,这样可以保证不重不漏。
令 \(f_{i,j}\) 表示前 \(i\) 个位置,上一个 \(1\) 在 \(j\) 位置得到的最大价值,显然转移有:
\(\begin{array}{c} &&&&&&&&&&f_{i,i}=\max_{j=1}^{i-1}f_{i-1,j}+\sum_{l_k\le i\wedge r_k=i}a_k \end{array}\)
\(\begin{array}{c} &&&&&&&&&&f_{i,j}&=f_{i-1,j}+\sum_{l_k\le j\wedge r_k=i}a_k\\ \end{array}\)
于是我们就有了 \(O(n^2)\) 的做法,滚动数组优化一下,空间复杂度为 \(O(n)\)。
思考优化,我们发现对于 \(i=j\),实际就是在 \([1,i-1]\) 查询了 \(f\) 的最大值,然后对于所有 \(r=i\) 的区间,它们都会对 \([l,r]\) 内的 \(\text{dp}\) 值产生 \(a\) 的贡献。
所以我们用线段树维护 \(f\),每到一个新位置执行两种操作:
- 查询全局最大值,更新 \(i\) 位置。
- 对于所有 \(r=i\) 的区间,在 \([l,r]\) 区间内加上 \(a\)。
区间加、区间最大值,线段树维护就好了,时间复杂度 \(O(nlogn)\)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define PII pair<int,int>
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=2e5+10;
int n,m;ll mx[N<<2],tag[N<<2];
vector<PII> d[N];
void pushdown(int u){
tag[u<<1]+=tag[u],tag[u<<1|1]+=tag[u];
mx[u<<1]+=tag[u],mx[u<<1|1]+=tag[u];
tag[u]=0;
}
void pushup(int u){mx[u]=max(mx[u<<1],mx[u<<1|1]);}
void modify(int u,int l,int r,int ql,int qr,ll v){
if(ql<=l&&r<=qr){
mx[u]+=v,tag[u]+=v;
return;
}
pushdown(u);
int mid=(l+r)>>1;
if(ql<=mid) modify(u<<1,l,mid,ql,qr,v);
if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,v);
pushup(u);
}
int main(){
n=rd,m=rd;
FOR(i,1,m){int l=rd,r=rd,v=rd;d[r].pb(mp(l,v));}
FOR(i,1,n){
modify(1,1,n,i,i,max(0ll,mx[1]));
for(auto j:d[i]) modify(1,1,n,j.fi,i,(ll)j.se);
}
printf("%lld\n",max(mx[1],0ll));
return 0;
}
X题
贪心 \(+\) \(\text{dp}\) 的好题。
有重量、体积、价值的关键字眼,考虑 \(01\) 背包,但发现暴力做是 \(O(n^2w)\) 的。
考虑贪心,对于两个物品 \(i,j\),假如先放 \(i\),后放 \(j\),则 \(i\) 后面还能放 \(s_i-w_j\) 重量的物品,反之则为 \(s_j-w_i\) 的物品。若先放 \(i\),则应满足 \(s_i-w_j>s_j-w_i\),即 \(s_i+w_i>s_j+w_j\)。所以按 \(s+w\) 从小到大排序,然后从上往下放,进行 \(01\) 背包的转移。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=10010;
int n;ll f[N<<2];
struct node{int w,s,v;}a[N];
bool cmp(node a,node b){return a.w+a.s<b.w+b.s;}
int main(){
n=rd;memset(f,-1,sizeof(f)),f[0]=0;
FOR(i,1,n) a[i].w=rd,a[i].s=rd,a[i].v=rd;
sort(a+1,a+1+n,cmp);
FOR(i,1,n) ROF(j,a[i].s,0){//i上面放了j的物品,现在加上i
f[j+a[i].w]=max(f[j+a[i].w],f[j]+a[i].v);
}
ll ans=0;
FOR(i,0,20000) ans=max(ans,f[i]);
printf("%lld\n",ans);
return 0;
}
Y题
经典计数题。
正难则反,考虑容斥思想。首先不考虑限制,从 \((1,1)\to (n,m)\) 方案数为 \(\begin{pmatrix}n+m-2\\n-1\end{pmatrix}\),然后减去经过限制点的方案数。
如何减去经过限制点的方案数呢?如果用朴素的容斥是 \(2\) 的次方级别的,复杂度无法接受,并且太麻烦了。
所以考虑 \(\text{dp}\),设 \(f_i\) 表示从 \((1,1)\) 到第 \(i\) 个限制点,中间不经过其他限制点的方案数。
我们先按坐标排序,然后进行转移:\(f_i=\begin{pmatrix}x_i+y_i-2\\x_i-1\end{pmatrix}-\sum_{j=1}^{i-1}f_j\times \begin{pmatrix}x_i-x_j+y_i-y_j\\x_i-x_j\end{pmatrix}\)。
我们发现这样可以不重不漏的统计从 \((1,1)\to (x_i,y_i)\) 的方案数。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=3010,M=2e5+10,mod=1e9+7;
int n,m,r,fac[M],infac[M],f[N];
struct node{int x,y;}a[N];
bool cmp(node a,node b){
if(a.x==b.x) return a.y<b.y;
return a.x<b.x;
}
int qpow(int a,int b){
int res=1;
while(b){if(b&1)res=1ll*res*a%mod;a=1ll*a*a%mod,b>>=1;}
return res;
}
void init(){
int t=2e5;fac[0]=1;
FOR(i,1,t) fac[i]=1ll*fac[i-1]*i%mod;
infac[t]=qpow(fac[t],mod-2);
ROF(i,t-1,0) infac[i]=1ll*infac[i+1]*(i+1)%mod;
}
int C(int n,int m){
if(n<0||m<0||n<m) return 0;
return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;
}
int main(){
init();
n=rd,m=rd,r=rd;FOR(i,1,r) a[i].x=rd,a[i].y=rd;
r++,a[r]={n,m};
sort(a+1,a+1+r,cmp);
FOR(i,1,r){
f[i]=C(a[i].x+a[i].y-2,a[i].x-1);
FOR(j,1,i-1){
f[i]=(1ll*f[i]-1ll*f[j]*C(a[i].x-a[j].x+a[i].y-a[j].y,a[i].x-a[j].x)%mod+mod)%mod;
}
}
printf("%d\n",f[r]);
return 0;
}
Z题
终于到最后一道题了,实际上就是一道斜率优化板子题。
朴素转移方程很好写: \(f_i=\min_{j=1}^{i-1}\{f_j+(h_i-h_j)^2+C\}\),暴力做是 \(O(n^2)\) 的。
把 \(\min\) 里面的式子拆开:
\(\begin{aligned} f_j+(h_i-h_j)^2+C&=f_j+h_i^2+h_j^2-2h_ih_j+C \\&=(f_j+h_j^2)-2h_jh_i+C+h_i^2 \end{aligned}\)
所以令 \(F_j=-2h_jx+h_j^2+f_j\),直接李超线段树,斜率优化啥的就滚远吧!
对于 \(i\),我们就找到 \(x=h_i\) 时纵坐标最小的点,然后进行更新即可。
时间复杂度 \(O(nlog^2n)\)。
如果你不会李超线段树,或者想追求更优的时间复杂度的话,由于本题 \(h\) 单调递增的特殊性质,还有 \(O(n)\) 的斜率优化做法。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define PDI pair<double,int>
#define mp make_pair
#define int long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=1e6+10,M=1e6;
const double eps=1e-10;
int n,h[N],seg[N<<2],C,f[N];
struct node{double k,b;}p[N];
int cmp(double x,double y){
if(x-y>eps) return -1;
if(y-x>eps) return 1;
return 0;
}
double cal(int id,int x){
return p[id].k*x+p[id].b;
}
void update(int u,int l,int r,int id){
int &g=seg[u],mid=(l+r)>>1;
if(cmp(cal(id,mid),cal(g,mid))==1) swap(g,id);
if(cmp(cal(id,l),cal(g,l))==1) update(u<<1,l,mid,id);
if(cmp(cal(id,r),cal(g,r))==1) update(u<<1|1,mid+1,r,id);
}
void modify(int u,int l,int r,int ql,int qr,int id){
if(ql<=l&&r<=qr){update(u,l,r,id);return;}
int mid=(l+r)>>1;
if(ql<=mid) modify(u<<1,l,mid,ql,qr,id);
if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,id);
}
PDI pmx(PDI a,PDI b){
if(cmp(a.first,b.first)>0) return a;
else return b;
}
PDI query(int u,int l,int r,int v){
if(l>v||r<v) return mp(0,0);
int mid=(l+r)>>1;PDI res=mp(cal(seg[u],v),seg[u]);
if(l==r) return res;
return pmx(res,pmx(query(u<<1,l,mid,v),query(u<<1|1,mid+1,r,v)));
}
signed main(){
n=rd,C=rd;
FOR(i,1,n) h[i]=rd;
p[0]={0,1e16};
p[1]={-2.0*h[1],1.0*h[1]*h[1]};
modify(1,1,M,1,M,1);
FOR(i,2,n){
int t=query(1,1,M,h[i]).second;
f[i]=p[t].k*h[i]+p[t].b+C+h[i]*h[i];
p[i]={-2.0*h[i],1.0*h[i]*h[i]+f[i]};
modify(1,1,M,1,M,i);
}
printf("%lld\n",f[n]);
return 0;
}

浙公网安备 33010602011771号