[做题笔记] 1373452814 的 dp 选讲
[AGC056B] Range Argmax
题目描述
给定 ,有 个 的子区间,第 个区间为
对于一个 阶排列 ,令 为 中最大值的下标。
排列可以任取,求本质不同的序列 的数量,答案对 取模。
解法
考虑得到序列 的方式,其实就是在笛卡尔树上 ,每次确定包含当前点区间的 ,然后把这些区间全部删掉。
得到删除序列是容易的,考虑如何把删除序列和 一一对应,只需要满足下面两个条件即可:
- 删除当前点之后,由于左右两边独立,强制先递归左边,再递归右边。
- 当前删除点是所有能得到这个 序列的删除方式中,位置最左的删除点。
翻译一下第二个条件,考虑当前点 的左儿子 ,设 表示包含 的区间最小的左端点,如果 ,那么显然先删除 也可以得到同样的 ;但如果 ,先删除 就会得到不同的 ,所以这会增加限制:
根据上面的限制来 ,设 表示只考虑导出区间 , 的方案数,转移:
其中 表示只考虑导出区间 ,包含 的区间最小的左端点。使用前缀和优化,时间复杂度
#include <cstdio>
const int M = 305;
const int MOD = 998244353;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,b[M],v[M][M],a[M][M][M],dp[M][M][M];
signed main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
int l=read(),r=read();
v[l][r]=1;
}
for(int r=1;r<=n;r++)
{
for(int l=1;l<=r;l++)
if(v[l][r]) b[l]=r;
for(int l=r;l>=1;l--)
{
for(int i=1;i<=n;i++)
a[l][r][i]=a[l+1][r][i];
for(int i=l;i<=b[l];i++)
a[l][r][i]=l;
}
}
for(int i=0;i<=n;i++) for(int l=1;l+i-1<=n;l++)
{
int r=l+i-1,f=0;
for(int i=l;i<=r;i++) f|=a[l][r][i];
if(!f)
{
for(int i=l-1;i<=r+1;i++)
dp[l][r][i]=1;
continue;
}
for(int i=l;i<=r;i++) if(a[l][r][i])
dp[l][r][i]=1ll*dp[l][i-1][a[l][r][i]]*
dp[i+1][r][i+1]%MOD;
for(int i=r-1;i>=l;i--)
dp[l][r][i]=(dp[l][r][i]+dp[l][r][i+1])%MOD;
}
printf("%d\n",dp[1][n][1]);
}
[AGC040E] Prefix Suffix Addition
题目描述
解法
首先考虑只有操作一时怎么做?考虑差分数组 ,那么操作一的效果是:把 减少 ,增加 ,并且满足增加的总和是
转化之后很容易得到答案下界,因为 都必须作为 被我们操作一次,所以答案下界就是 ,因为序列非负,所以每个位置的差分后缀和非正,那么只要我们贪心地增加靠后的 ,就可以构造出解。
只有操作二时是类似的,答案下界是
由于操作一和操作二是独立的,可以把他们分开来考虑。问题可以转化成:找到一种拆分方式 ,最小化代价:
设 表示考虑前 个位置,满足 的最小代价。但是这样状态数就直接爆炸,可以搞一些 :
- 观察代价的形式,发现 随着 的增大而增大。
- 由于上一个转移点是任意的,所以 的极差不超过
那么我们可以用一个分段函数来替代我们的 数组,也就是维护 ,表示现在的基准值(最小 值)是 ,满足 时取 ; 时取 ; 时取
如果 , 这一段可以直接平移下来,不会有额外的贡献,但是基准值的范围也不能右移,因为右移后如果又要从上一层的 转移过来会有额外的贡献。
考虑 这个点的对基准值 的影响,由于不能有 的贡献,即必须满足 ,解得 ,那么 基准值 的范围就变成了
如果 ,为了避免 的贡献,基准值的范围变成了 ,基准值 的范围变成了 ,注意如果新的 ,那么要把基准值增加 ,并且让
最后还要添加 的贡献,即 ,时间复杂度
总结
最后讨论法优化 的技巧很厉害,类似的题目还有:Split
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 200005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,f0,f1,dp,ls,a[M];
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
int v=read();
if(v>=ls) f1=max(f1,f0+v-ls);
else
{
f1=max(f0,f1-ls+v);
f0+=v-ls;
if(f0<0) dp++,f0=f1,f1=v;
}
f0=min(f0,v);f1=min(f1,v);ls=v;
}
printf("%d\n",dp+(ls>f0));
}
Evacuation
题目描述
有 排成一列,第 个位置可以容纳 个人。给出一个非负整数 ,定义一个位置的代价为初始有 个人在这个位置上,可以让人移动到其他位置,所有人都可以被容纳的最小总移动距离。
有 次询问,每次给出区间 ,一个人走到区间外视为被容纳,问 所有位置代价的最大值。
解法
一些人会走到边界外,但是只会走到最近的一个边界外。所以对于询问 , 会把 当成边界, 会把 当成边界。设 表示初始 个人都在 位置,左边界是 的最小移动总距离,它可以表示成:
可以把 修正到不会取 (预处理 表示最小的不会取 的 ),正贡献是 ,对于负贡献,中间的 贡献 次,并且依次向两边递减。预处理 就可以 计算任意的
一个重要的 是:对于 , 满足决策单调性,即 增大时最优 的取值不降,证明:
只需要证明 即可,由于:
可以发现第一个求和区间被第二个包含,那么上面显然更大,所以有:
移项之后就可以得到决策单调性的关键式子:
由于询问可以转化成:对于固定的 , 被限制在一个区间内的最大代价。我们可以把所有询问拆分到线段树上,然后对于线段树上的一个节点,暴力跑分治实现的决策单调性。
对于右边界 的情况,只需要把所有东西都翻转,然后同样跑一次就行了,时间复杂度
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 200005;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,q,a[M],qx[M],qy[M],ans[M];
int s[M],sum[M],r[M];vector<int> v,p[M<<2];
void ins(int i,int l,int r,int L,int R,int x)
{
if(L>r || l>R) return ;
if(L<=l && r<=R) {p[i].push_back(x);return ;}
int mid=(l+r)>>1;
ins(i<<1,l,mid,L,R,x);
ins(i<<1|1,mid+1,r,L,R,x);
}
int ask(int l,int x)
{
l=max(l,x-r[x]+1);
return (m-s[2*x-l]+s[l-1])*(x-l+1)
+x*(s[x]-s[l-1])-(sum[x]-sum[l-1])
-x*(s[2*x-l]-s[x])+(sum[2*x-l]-sum[x]);
}
void cdq(int l,int r,int ql,int qr)
{
if(ql>qr) return ;
int mid=(ql+qr)>>1,id=v[mid],p=l,mx=0;
for(int i=l;i<=r;i++) if(ask(qx[id],i)>mx)
mx=ask(qx[id],i),p=i;
ans[id]=max(ans[id],mx);
cdq(l,p,ql,mid-1);cdq(p,r,mid+1,qr);
}
int cmp(int x,int y) {return qx[x]<qx[y];}
void dfs(int i,int l,int r)
{
if(!p[i].empty())
{
v=p[i];sort(v.begin(),v.end(),cmp);
cdq(l,r,0,v.size()-1);
}
if(l==r) return ;
int mid=(l+r)>>1;
dfs(i<<1,l,mid);dfs(i<<1|1,mid+1,r);
}
void work()
{
for(int i=1;i<=4*n;i++) p[i].clear();
for(int i=1;i<=q;i++)
ins(1,1,n,qx[i],(qx[i]+qy[i])/2,i);
for(int i=1;i<=n;i++)
s[i]=s[i-1]+a[i],sum[i]=sum[i-1]+i*a[i];
for(int i=1;i<=n;i++)
{
r[i]=max(r[i-1]-1,1ll);
while(i+r[i]<=n && i>r[i] &&
s[i+r[i]]-s[i-r[i]-1]<=m) r[i]++;
}
dfs(1,1,n);
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=read();
q=read();
for(int i=1;i<=q;i++) qx[i]=read(),qy[i]=read();
work();
reverse(a+1,a+1+n);
for(int i=1;i<=q;i++) qx[i]=n-qx[i]+1,
qy[i]=n-qy[i]+1,swap(qx[i],qy[i]);
work();
for(int i=1;i<=q;i++) printf("%lld\n",ans[i]);
}
[AGC049E] Increment Decrement
题目描述
解法
首先考虑对于固定的 ,如何计算答案。
如果只有第一种操作,那么答案
如果只有第二种操作,考虑差分数组,不难发现答案为
把这两种操作的影响拆分,设第二种操作对位置 的影响为 ,则问题转化成找到一组 ,最小化:
设 表示考虑前 个位置, 的最小代价,转移:
略微调整一下状态定义,把 这个代价的计算放在 处,转移变成:
初始化 ,答案是
可以用归纳法证明 是凸函数,首先 是凸函数,然后考虑 向 转移时,首先加上了一个绝对值函数 ,然后又和 进行了 卷积,执行这些操作后还是凸函数。
这时候就可以直接上 了,考虑维护 处的点值 和描述斜率变化的集合 ,对于添加 这个绝对值函数,我们首先把 加上 ,然后向 中添加两个 处的转折点。
稍微麻烦一下的操作是和 进行 卷积。回忆两个凸函数是如何进行 卷积的,实际上就是类似归并排序,选择添加增量少的部分。效果如下图,一句话概括就是掐头去尾:
我们取出最小的转折点 和最大的转折点 ,首先把 减去 ,从 中删除 就可以使得开头段的斜率变为 ;然后从 中删除 就可以使得结尾段的斜率变为
现在回到计数问题上来,发现加的部分是固定的,主要的减的部分不好计算。可以考虑拆贡献,我们把所有权值 都离散化,每次把小于 的权值设置为 ,把 的权值设置为 ,只需要统计取出 的个数,乘上 就是贡献。
设 表示前 次操作,现在 中有 个 的方案数, 表示对应取出 的总个数。
转移就暴力添加 ,模拟 的过程即可,时间复杂度
总结
如果各种操作独立,可以单独分析各种操作,然后枚举不同操作的影响,就可以方便地计算代价。
看到 等凸函数复合时,一定要本能地联想到
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 105;
const int MOD = 1e9+7;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,c,k,ans,sub,h[M],f[M][M],g[M][M];
pair<int,int> b[M*M];
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
n=read();c=read();k=read();
for(int i=1;i<=n;i++) for(int j=1;j<=k;j++)
{
int x=read();add(ans,x);
b[(i-1)*k+j]=make_pair(x,i);
}
sort(b+1,b+1+n*k);
for(int w=1;w<=n*k;w++)
{
for(int i=0;i<=n;i++) for(int j=0;j<=n;j++)
f[i][j]=g[i][j]=0;
f[0][c]=1;
for(int i=1;i<=n;i++) for(int j=0;j<=c;j++)
for(int t=0;t<2;t++)
{
int vl=t?h[i]:k-h[i],tj=j+2*t;
int f1=f[i-1][j]*vl%MOD,f2=g[i-1][j]*vl%MOD;
if(tj) tj--;
else f2=(f2+f1)%MOD;
tj=min(tj,c);
add(f[i][tj],f1);add(g[i][tj],f2);
}
for(int i=0;i<=n;i++)
add(sub,g[n][i]*(b[w].first-b[w-1].first));
h[b[w].second]++;
}
for(int i=1;i<n;i++) ans=ans*k%MOD;
printf("%lld\n",(ans-sub+MOD)%MOD);
}
[JOISC 2017 Day 2] 火车旅行
题目描述
解法
考虑两个点 可以一步到达的条件:
我们从 较小的一侧考虑,对于每个 找到左边第一个权值大于等于它的点 ,右边第一个权值大于等于它的点 ,那么我们连边 和 ,在这个图上跑最短路就可以得到答案。
暴力最短路显然会超时,考虑换一种方式来构建这张图——按照权值从大到小加入点,每次添加若干条链。比如对于样例,我们先加入 1-8-9
,再加入 1-5-6-7-8
,最后加入 1-2-3-4-5
,把边当成点,会构成这样的树形结构:
发现我们从某个点走若干步时,一定会经过树上祖先的两点之一。把问题转化到树上时,我们只需要维护到左端点的最小距离和到右端点的最小距离,跳一步父亲相当于乘上一个 的矩阵转移。
那么对于 两点的询问,我们先让它们跳到 的儿子处,然后在环上找最短距离即可。
由于本题的边权为 ,所以两个步骤可以合为一体。首先倍增预处理 表示跳 可以到达的最远左端点 最远右端点,处理询问时,如果覆盖范围没有到另一个点,就直接暴力跳。
时间复杂度
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,q,a[M],s[M],L[M][20],R[M][20];
signed main()
{
n=read();read();q=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=n;i++)
{
while(m && a[s[m]]<a[i]) m--;
L[i][0]=m?s[m]:i;s[++m]=i;
}
m=0;
for(int i=n;i>=1;i--)
{
while(m && a[s[m]]<a[i]) m--;
R[i][0]=m?s[m]:i;s[++m]=i;
}
L[0][0]=0;R[n+1][0]=n+1;
for(int j=1;j<20;j++)
for(int i=0;i<=n+1;i++)
{
L[i][j]=min(L[L[i][j-1]][j-1],L[R[i][j-1]][j-1]);
R[i][j]=max(R[L[i][j-1]][j-1],R[R[i][j-1]][j-1]);
}
while(q--)
{
int a=read(),b=read(),ans=0;
if(a>b) swap(a,b);
int l=a,r=a;
for(int i=19;i>=0;i--)
{
int nl=min(L[l][i],L[r][i]);
int nr=max(R[l][i],R[r][i]);
if(nr<b) l=nl,r=nr,ans+=(1<<i);
}
a=r;l=r=b;
for(int i=19;i>=0;i--)
{
int nl=min(L[l][i],L[r][i]);
int nr=max(R[l][i],R[r][i]);
if(a<nl) l=nl,r=nr,ans+=(1<<i);
}
printf("%d\n",ans);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通