137_345_2814 dp 杂题选讲
#2772. 「ROI 2017 Day 2」反物质
有 \(n\) 种实验,第 \(i\) 种实验一次的费用为 \(c_{i}\) ,这种实验会随机生成 \(\left[l_{i}, r_{i}\right]\) 中一个整数数量的反物质。你可以存储 \(k\) 个单位的反物质。你进行的不能超过存储上界,即如果当前你有 \(x\) 个单位的反物质,则你只能选择 \(r_{i} \leq k-x\) 的实验。
你可以进行任意数量的实验,也可以在任何时候停止。如果最后生成了 \(x\) 个单位的反物质,则收益为 \(10^{9} * x\) 减去实验的总费用。
你需要选择策略,最大化最坏情况的收益。求最坏情况收益的最大值。
\( \begin{array}{l} n \leq 100, k \leq 2 \times 10^{6}, 1 \leq l_{i} \leq r_{i} \leq k, c_{i} \leq 100 \\ 1 s, \text{40MB} \end{array}\)
可以列出 \(dp\) 式子:
显然 \(O(nk)\) 可过,考虑快速求出 \(\left(\min _{x=i+l_{j}}^{i+r_{j}} d p_{x}\right)\),可以开 \(k\) 个单调队列或使用 \(\text{st}\) 表,但是空间是 \(O(nk)\) 或 \(O(n\log n)\) 的,会被卡。
我们考虑单调队列的做法,发现队列会一直从左边加数,将右侧的数弹出,最小值的位置一定是不断向左移动的。
因此如果能快速维护最小值位置的左移,则总时间复杂度仍然是 \(O(nk)\) 的。
对于每个位置,单调栈求出它左边第一个小于它的位置,对每种反应维护它当前最小值所在位置,然后从当前区间右端点开始向左跳,直到下一个最小值在区间外即可。
时间复杂度 \(O(n k)\) ,空间复杂度 \(O(n+k)\) ,空间不超过 \(\text{32MB}\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long dp[2000005];
int stk[2000005],top,L[2000005],pos[105];
int l[105],r[105],c[105];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d%d%d",&l[i],&r[i],&c[i]);
for(int i=1;i<=n;i++)pos[i]=m+1;
for(int i=m;~i;i--){
dp[i]=i*1e9;
for(int j=1;j<=n;j++){
pos[j]=min(pos[j],i+r[j]);
while(L[pos[j]]>=i+l[j])pos[j]=L[pos[j]];
dp[i]=max(dp[i],dp[pos[j]]-c[j]);
}
while(top&&dp[stk[top]]>dp[i])L[stk[top--]]=i;
stk[++top]=i;
}
printf("%lld\n",dp[0]);
return 0;
}
AGC048D Pocky Game
考虑一个区间 \([l,r]\) 当前来到左边先手,假如先手必胜,那么如果最左边的石子增多了,先手仍然必胜。如果石子减少了,先手就未必必胜了,那么显然存在一个 \(L[l][r]\) 使得 \((l,r]\) 内的的石子不变,当 \(a_l\ge L[l][r]\) 时先手必胜,反之先手必败。
类似地,可以定义 \(R[l][r]\) 表示当前来到右边先手, \([l,r)\) 内的的石子不变,当 \(a_r\ge R[l][r]\) 时先手必胜,反之先手必败。
考虑转移,假如 \(a_r<R[l+1][r]\),显然 \(L[l][r]=1\) 。
如果 \(a_r\ge R[l+1][r]\) ,那么 \(a_l\) 需要将 \(a_r\) 消耗到 \(R[l+1][r]\) 以下,此时如果左手先将左边取空,那么博弈会进入 \([l+1,r]\) ,而当前 \(a_r<R[l+1][r]\) ,右手必败。
因此右手只能将右边取空使得博弈进入区间 \([l,r-1]\) ,此时只需使得 \(a_l\) 剩余部分不小于 \(L[l][r-1]\) 即可。
进而有:
类似地处理 \(R[l][r]\) ,区间 \(dp\) 转移即可,最后判断 \(L[1][n]\) 和 \(a_1\) 的关系即可得出答案,时间复杂度 \(O(n^2)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int T;
int n,a[105];
long long L[105][105],R[105][105];
inline void solve(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)L[i][i]=1;
for(int i=1;i<=n;i++)R[i][i]=1;
for(int len=1;len<=n;len++){
for(int l=1;l+len<=n;l++){
int r=l+len;
if(R[l+1][r]>a[r])L[l][r]=1;
else L[l][r]=a[r]-R[l+1][r]+1+L[l][r-1];
if(L[l][r-1]>a[l])R[l][r]=1;
else R[l][r]=a[l]-L[l][r-1]+1+R[l+1][r];
}
}
puts(L[1][n]<=a[1]?"First":"Second");
}
int main(){
scanf("%d",&T);
while(T--)solve();
return 0;
}
AGC056B Range Argmax
给定 \(n, m\) ,有 \(m\) 个 \([1, n]\) 的子区间,第 \(i\) 个区间为 \(\left[l_{i}, r_{i}\right]\) 。
对于一个 \(n\) 阶排列 \(p\) ,令 \(x_{i}\) 为 \(p_{l_i} \ldots p_{r_i}\) 中最大值的下标。
排列可以任取,求本质不同的序列 \(\{x\}\) 的数量,答䅁对 \(998244353\) 取模。
\(n \leq 300\)
考虑得到序列 \(\{x\}\) 的方式,其实就是在笛卡尔树上 \(\mathrm{dfs}\) ,每次确定包含当前区间最大值的位置,然后把当前包含这个位置的区间全部删掉,剩余的递归下去。
但是这道题中 \(p_i\) 表示的是最大值的下标,而不是最大值,因此如果有两个位置 \(a,b\) 分别是某些区间的最大值,而又不能通过区间之间的关系推断出哪个更大,那么无论 \(a,b\) 哪个更大最终得到的序列都是相同的。
因此不同的删除序列可能对应相同的答案,为此我们可以每次只删除所有能得到这个 \(\{x\}\) 序列的删除方式中,位置最左的删除点。
考虑当前点 \(a\) 的左儿子 \(b\) ,设 \(lb\) 表示包含 \(a\) 的区间最小的左端点,如果 \(b<lb\) ,那么显然先删除 \(b\) 也可以得到同样的 \(\{x\}\) 。
但如果 \(b \geq l b\) ,先删除 \(b\) 就会得到不同的 \(\{x\}\) ,所以这会增加限制: \(b \geq lb\)
根据上面的限制来 \(dp\) ,设 \(dp[l][r][x]\) 表示只考虑导出区间 \([l, r]\) ,\(a \geq x\) 的方案数,转移:
其中 \(lb[l][r][a]\) 表示只考虑导出区间 \([l, r]\) ,包含 \(a\) 的区间最小的左端点。使用前缀和优化,时间复杂度 \(O\left(n^{3}\right)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int md=998244353;
int L[100005],R[100005],dp[305][305][305],lim[305][305][305];
bool vis[305][305][305];
int dfs(int l,int r,int x){
if(l>r)return 1;
if(vis[l][r][x])return dp[l][r][x];
vis[l][r][x]=1;
bool flag=0;
for(int i=l;i<=r;i++)if(lim[l][r][i]!=1e9)flag=1;
if(!flag)return dp[l][r][x]=1;
if(x>r)return 0;
int res=(x<r?dfs(l,r,x+1):0);
if(lim[l][r][x]!=1e9)res=(res+1ll*dfs(l,x-1,lim[l][r][x])*dfs(x+1,r,x+1))%md;
return dp[l][r][x]=res;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)scanf("%d%d",&L[i],&R[i]);
for(int l=1;l<=n;l++){
for(int r=l;r<=n;r++){
for(int i=l;i<=r;i++)lim[l][r][i]=1e9;
}
}
for(int i=1;i<=m;i++){
for(int j=L[i];j<=R[i];j++){
lim[L[i]][R[i]][j]=min(lim[L[i]][R[i]][j],L[i]);
}
}
for(int i=1;i<=n;i++){
for(int l=i;l;l--){
for(int r=i;r<=n;r++){
if(i!=l)lim[l][r][i]=min(lim[l][r][i],lim[l+1][r][i]);
if(i!=r)lim[l][r][i]=min(lim[l][r][i],lim[l][r-1][i]);
}
}
}
printf("%d\n",dfs(1,n,1));
return 0;
}
ARC119F AtCoder Express 3
先考虑如果所有点的颜色都被确定,如何快速求出一条从 \(0\to n\) 的最短路径。
考虑简单的 \(\text{DP}\)。设 \(dp_{i,\alpha,\beta}\) 为车站 \(0\sim i\) 中,到达最后一个红色车站的最短路径长度为 \(\alpha\),到达最后一个蓝色车站的最短路径长度为 \(\beta\) 是否可行。为方便,下文简记作 \((\alpha,\beta)\) 。
那么有如下转移:
-
当 \(c_{i}=\texttt{A},c_{i-1}=\texttt{A}\) 时,\((\alpha,\beta)\to (\alpha+1,\beta)\) 。
-
当 \(c_i=\texttt{A},c_{i-1}=\texttt{B}\) 时,\((\alpha,\beta)\to(\min(\alpha,\beta)+1,\min(\beta,\min(\alpha,\beta)+1+1))\) 。
-
当 \(c_i=\texttt{B},c_{i-1}=\texttt{B}\) 时,\((\alpha,\beta)\to (\alpha,\beta+1)\) 。
-
当 \(c_i=\texttt{B},c_{i-1}=\texttt{A}\) 时,\((\alpha,\beta)\to (\min(\alpha,\min(\beta,\alpha)+1+1),\min(\beta,\alpha)+1)\) 。
为什么呢?我们发现尽管可以双向通行,但对于车站 \(i\),到达一个车站 \(j(j<i)\),一定只通过一条 \(x\to x+1\) 的普通路径。如果通过更多条,或不通过普通路径而通过蓝色点,红色点之间的路径,一定不会更优。正确性基于一定不会重复通过同一个车站两次。所以我们可以直接通过一个红色车站去尝试更新蓝色车站的值,或通过一个蓝色车站去尝试更新红色车站的值。
有了这么一个求最短路径的 \(\text{DP}\),就可以进一步尝试设计一个求方案数的 \(\text{DP}\) 了。设 \(dp_{i,\alpha,\beta,pre}\) 为车站 \(0\sim i\) 中,到达最后一个红色车站的最短路径长度为 \(\alpha\),到达最后一个蓝色车站的最短路径长度为 \(\beta\) ,上一个车站的颜色为 \(pre\) 的方案数。
转移其实是很类似的:
-
当 \(c_{i}\neq\texttt{B},pre=0\) 时,\((i-1,\alpha,\beta,0)\to (i,\alpha+1,\beta,0)\) 。
-
当 \(c_i\neq\texttt{B},pre=1\) 时,\((i-1,\alpha,\beta,1)\to (i,\min(\alpha,\beta)+1,\min(\beta,\min(\alpha,\beta)+1+1),0)\) 。
-
当 \(c_i\neq\texttt{A},pre=1\) 时,\((i-1,\alpha,\beta,1)\to (i,\alpha,\beta+1,1)\) 。
-
当 \(c_i\neq\texttt{A},pre=0\) 时,\((i-1,\alpha,\beta,0)\to (i,\min(\alpha,\min(\beta,\alpha)+1+1),\min(\beta,\alpha)+1,0)\) 。
根据 \(c_1\) 的取值赋上初值即可。但是状态数为 \(O(n^3)\),无法通过。
考虑下面这种情况,蓝色点很多,但对答案产生影响的事实上只有最后一个蓝色点,所以我们可以不考虑之前所有蓝色点的取值,也就是说,我们可以强加一个取值,在这种情况下是 \(6\)。
更为一般的,当 \(\alpha\geq \beta+2\) 时,我们令 \(\alpha=\beta+2\),当 \(\beta\geq \alpha+2\) 时,我们令 \(\beta=\alpha+2\) 。这样我们就限定了 \(-2\leq\beta-\alpha\leq 2\),将状态数减少到了 \(O(n^2)\) 。
具体实现中,我们可以定义 \(dp[i,\alpha,\beta-\alpha+2,pre]\) 这样的状态,并滚动掉 \(i\) 这一维,压缩掉 \(pre\) 这一维。
并且由于有 \(-2\leq \beta-\alpha\leq 2\),所以状态转移可以简化为:
-
当 \(c_{i}\neq\texttt{B},pre=0\) 时,\((i-1,\alpha,\beta,0)\to (i,\min(\alpha+1,\beta+2),\beta,0)\) 。
-
当 \(c_i\neq\texttt{B},pre=1\) 时,\((i-1,\alpha,\beta,1)\to (i,\min(\alpha,\beta)+1,\beta,0)\) 。
-
当 \(c_i\neq\texttt{A},pre=1\) 时,\((i-1,\alpha,\beta,1)\to (i,\alpha,\min(\beta+1,\alpha+2),1)\) 。
-
当 \(c_i\neq\texttt{A},pre=0\) 时,\((i-1,\alpha,\beta,0)\to (i,\alpha,\min(\beta,\alpha)+1,0)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,k;
int dp[4005][4005][5][2];
char s[4005];
const int md=1e9+7;
int main(){
scanf("%d%d",&n,&k);
scanf("%s",s+1);--n;
if(s[1]!='A')dp[1][0][3][1]=1;
if(s[1]!='B')dp[1][1][1][0]=1;
for(int i=1;i<n;i++){
for(int j=0;j<=n;j++){
for(int d=0;d<=4;d++){
int a=j,b=j+d-2;
if(s[i+1]!='B'){
{
int ua=min(a+1,b+2),ub=b;
dp[i+1][ua][ub-ua+2][0]=(dp[i+1][ua][ub-ua+2][0]+dp[i][j][d][0])%md;
}
{
int ua=min(a+1,b+1),ub=min(b,a+2);
dp[i+1][ua][ub-ua+2][0]=(dp[i+1][ua][ub-ua+2][0]+dp[i][j][d][1])%md;
}
}
if(s[i+1]!='A'){
{
int ua=a,ub=min(a+2,b+1);
dp[i+1][ua][ub-ua+2][1]=(dp[i+1][ua][ub-ua+2][1]+dp[i][j][d][1])%md;
}
{
int ua=min(a,b+2),ub=min(b+1,a+1);
dp[i+1][ua][ub-ua+2][1]=(dp[i+1][ua][ub-ua+2][1]+dp[i][j][d][0])%md;
}
}
}
}
}
int res=0;
for(int i=0;i<=n;i++){
for(int d=0;d<=4;d++){
int a=i,b=i+d-2;
if(min(a,b)+1<=k)res=(res+(dp[n][i][d][0]+dp[n][i][d][1])%md)%md;
}
}
printf("%d\n",res);
return 0;
}
AT2538 铁道旅行 (Railway Trip)
有 \(n\) 个地点排成一列,每个地点有一个 \([1, k]\) 间的整数权值 \(v_{i}\) ,保证 \(v_{1}=v_{n}=k\) 。
有 \(k\) 种列车,所有列车都沿着 \(n\) 个地点排成的列双向行驶。第 \(j\) 种列车只停靠所有满足 \(v_{i} \geq j\) 的地点 \(i\) 。
当你在一个地点时,你可以选择任意一种停靠该地点的列车,选择任意一个方向,前往这种列车往这个方向行驶时停靠的下一个地点,这样一次操作的代价为 \(1\) 。
\(q\) 次询问,每次给出 \(s, t\) ,询问从 \(s\) 到 \(t\) 的最小代价。
\(n, k, q \leq 2 \times 10^{5}\)
bonus: 在点 \(x\) 处,向左和向右分别有代价 \(l c_{x}\),\(r c_{x}\) ,代价为正整数。
\(3 s, 1024 \text{MB}\)
考虑两个点直接可以直接到达的条件,不难发现 \(i, j(i<j)\) 间可以直接到达当且仅当 \(\max _{i=i+1}^{j-1} v_{i}<\min \left(v_{i}, v_{j}\right)\) 。
如果在 \(v_{i}, v_{j}\) 较小的一侧考虑,不妨设 \(v_{i} \leq v_{j}\) ,则可以发现 \(j\) 一定是 \(i\) 向右第一个权值大于等于它的点。
将这看成一张图,则可以使用如下方式加入所有边: 对于每个 \(i\) 找到两侧分别第一个权值大于等于它的点 \(l_{i}, r_{i}\) ,连边 \(\left(l_{i}, i\right),\left(r_{i}, i\right)\) 。
我们已经求出,在花费为 \(1\) 时,从 \(i\) 号节点出发,最左可以到达 \(l_i\) 号节点,最右可以到达 \(r_i\) 号节点。
那么在花费为 \(k\) 的时候,从 \(i\) 号节点最左和最右可以到达的点怎么求呢?
容易发现这个可以倍增来算。
现在我们把这些区间展开成一棵树:如果区间 \(a\) 完全包含了区间 \(b\),且区间 \(a\) 代表了从 \(i\) 出发跳 \(k\) 次覆盖的区间,区间 \(b\) 代表了从 \(i\) 出发跳 \(k−1\) 次覆盖的区间,那么我们将 \(b\) 挂在 \(a\) 下面。
现在来转化问题:我们现在让 \([A,A]\) 和 \([B,B]\) 两个点向上跳,直到它们的父节点相同为止(也就是说这两个区间紧挨着,别忘了起点和终点不算入答案)。对于原题,我们跳的次数就是答案,对于 bonus 则需要维护到左端点的最短距离和到右端点的最短距离,跳一步父亲相当于乘上一个 \(2×2\) 的矩阵转移,时间复杂度 \(O(q\log n)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,k,q;
int a[100005];
int pre[17][100005],nxt[17][100005],stk[100005],top;
int main(){
scanf("%d%d%d",&n,&k,&q);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
while(top&&a[stk[top]]<=a[i])nxt[0][stk[top--]]=i;
stk[++top]=i;
}
while(top)nxt[0][stk[top--]]=n+1;nxt[0][n+1]=n+1;
for(int i=n;i>=1;i--){
while(top&&a[stk[top]]<=a[i])pre[0][stk[top--]]=i;
stk[++top]=i;
}
for(int i=1;i<17;i++){
for(int j=1;j<=n+1;j++){
pre[i][j]=min(pre[i-1][pre[i-1][j]],pre[i-1][nxt[i-1][j]]);
nxt[i][j]=max(nxt[i-1][nxt[i-1][j]],nxt[i-1][pre[i-1][j]]);
}
}
while(q--){
int a,b,res=0;scanf("%d%d",&a,&b);
if(a>b)swap(a,b);
{
int l=a,r=a;
for(int i=16;~i;i--){
if(max(nxt[i][l],nxt[i][r])<b){
int tl=min(pre[i][l],pre[i][r]),tr=max(nxt[i][l],nxt[i][r]);
l=tl;r=tr;res+=(1<<i);
}
}
a=r;
}
{
int l=b,r=b;
for(int i=16;~i;i--){
if(min(pre[i][l],pre[i][r])>a){
int tl=min(pre[i][l],pre[i][r]),tr=max(nxt[i][l],nxt[i][r]);
l=tl;r=tr;res+=(1<<i);
}
}
b=l;
}
printf("%d\n",res);
}
return 0;
}
[AGC040E] Prefix Suffix Addition
考虑只有第一种操作的情况。考虑差分,令 \(b_{i}=x_{i}-x_{i-1}\) ,则第一种操作相当于让 \(b_{k}\) 减去 \(c_{k}\) ,然后让 \(b_{0}, \cdots, b_{k-1}\) 加上一些数,加的数的总和为 \(c_{k}\) 。
考虑最后的序列的差分,那么每个差分为负数的位置必须至少操作一次,同时我们只要对每个以差分为负数的位置开头的后缀操作一次一定可以达到目标。
因此如果只使用第一种操作,答案为 \(\sum_{i=0}^{n}\left[a_{i}>a_{i+1}\right]\) 。第二种操作类似。
考虑第一种操作增加了多少,可以将原问题变为如下问题:
你需要将每个 \(a_{i}\) 分成非负整数 \(b_{i}, c_{i}\) ,使得 \(b_{i}+c_{i}=a_{i}\) ,最小化如下代价:
直接的做法是设 \(d p_{i, j}\) 表示考虑了前 \(i\) 个位置,且 \(b_{i}=j\) 时前面的最小代价,这样是 \(O(n v)\) 的。\(\text{DP}\) 式子可以写成这样的形式:
由于是取 \(\min\) ,对于一个 \(j\) ,只有当 \(k\) 减小时答案才可能会减小,所以对于取值相同的一段 \(f_{i-1, k}\) 来说,\(k\) 越小,答案一定越优,所以我们只需要考虑每段区间的最左边的端点就行,于是只用考虑三个左端点即可。
设这三个区间为:
那么式子其实就是:
设 \(\Delta=a_{i}-a_{i-1}\) ,那么就是:
我们分两种情况来讨论:
- \(\Delta \geq 0\)
可以画个图来看:
设新的 \(p, q\) 为 \(p^{\prime}, q^{\prime}\) 。
发现,\(d\) 的取值一定是 \(\left[q+\Delta+1, a_{i}\right]\) ,即 \(q^{\prime}=q+\Delta\) 。
\(p \leq q\) ,那么此时只需要在 \(q\) 和 \(p+\Delta\) 中取 \(\min\) 即可,即 \(p^{\prime}=\min (q, p+\Delta)\) 。
发现此时 \(p^{\prime}, q^{\prime} \leq a_{i}\) 是一定成立的,因为 \(p \leq a_{i-1}, p^{\prime} \leq p+\Delta, p+\Delta \leq a_{i-1}+\Delta=a_{i}\) ,\(q^{\prime}\) 同理,所以不需要考虑超出边界的问题。
- \(\Delta<0\)
同样的,画出图来:
和上面是一样的,\(p^{\prime}=\min (q+\Delta, p), q^{\prime}=q\) 。
但是这里需要注意,\(q\) 可能大于等于 \(a_{i}\) ,这时候答䅁为 \(d\) 的区间就不存在了,此时的最小值变成了 \(d+1\) ,所以需要更新一下 \(d\) 的取值。
还需要注意一下 \(q+\Delta<0\) 的情况。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,a[200005];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
int p=-1,q=-1,d=0;
for(int i=1;i<=n+1;i++){
int delta=a[i]-a[i-1];
if(delta>=0){
p=min(q,p+delta);
q+=delta;
}
else {
p=max(min(q+delta,p),-1);
if(q>=a[i]){
d++;
q=p,p=-1;
}
}
}
printf("%d\n",d);
return 0;
}
gym 102586B Evacuation
有 \(1,2 \ldots n\) 排成一列,第 \(i\) 个位置可以容纳 \(a_{i}\) 个人。给出一个非负整数 \(m\) ,定义一个位置的代价为初始有 \(m\) 个人在这个 位置上,可以让人移动到其他位置,所有人都可以被容纳的最小总移动距离。
有 \(q\) 次询问,每次给出区间 \([l, r]\) ,一个人走到区间外视为被容纳,问 \([l, r]\) 所有位置代价的最大值。
\(n, q \leq 2 \cdot 10^{5}\)
一些人会走到边界外,但是只会走到最近的一个边界外。所以对于询问 \([l, r]\) ,\(x \in[l, m i d]\) 会把 \(l\) 当成边界,
\(x \in(\operatorname{mid}, r]\) 会把 \(r\) 当成边界。设 \(f(x, l)\) 表示初始 \(m\) 个人都在 \(x\) 位置,左边界是 \(l\) 的最小移动总距离,它可以表示成:
可以把 \(l\) 修正到不会取 \(0\) (预处理 \(r[x]\) 表示最小的不会取 \(0\) 的 \(l\) ),正贡献是 \(m \cdot(x-l+1)\),对于负贡献,中间的 \(x\) 贡献 \(x-l+1\) 次,并且依次向两边递减。预处理 \(s[x]=\sum_{i=1}^{x} a_{i}, \operatorname{sum}[x]=\sum_{i=1}^{x} i \cdot a_{i}\) 就可以 \(O(1)\) 计算任意的 \(f\) 。
一个重要的 observation 是:对于 \(l\) ,\(x\) 满足决策单调性,即 \(l\) 增大时最优 \(x\) 的取值不降,证明:
只需要证明 \(f(x, l)+f(x+1, l+1) \geq f(x, l+1)+f(x+1, l)\) 即可,由于:
可以发现第一个求和区间被第二个包含,那么上面显然更大,所以有:
移项之后就可以得到决策单调性的关键式子:
由于询问可以转化成:对于固定的 \(l\) ,\(x\) 被限制在一个区间内的最大代价。我们可以把所有询问拆分到线段树上,然后对于线段树上的一个节点,暴力跑分治实现的决策单调性。
不过这个单调性是之关于 \(x,l\) 的,对于一个询问 \([L,R]\) ,设 \(mid=\dfrac{L+R}{2}\) ,如果决策点在 \([mid+1,R]\) 中,答案会受到右端点的干扰进而不满足决策单调性,因此我们只能处理决策点在 \({L,mid]\) 中的情况。对于另一种情况,我们只需将序列反转做一遍,两种情况答案取 \(\min\) 即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,q;
long long m,a[200005],s[200005],sum[200005];
int lim[200005];
inline long long Get(int x,int l){
l=max(l,x-lim[x]+1);int r=2*x-l;
// cout<<x<<" "<<l<<" Get "<<m*(x-l+1)<<" "<<(s[x]-s[l-1])-(l-1)*(sum[x]-sum[l-1])<<" "<<(r+1)*(sum[r]-sum[x])-s[r]+s[x]<<endl;
// cout<<m*(x-l+1)-(s[x]-s[l-1])+(l-1)*(sum[x]-sum[l-1])-(r+1)*(sum[r]-sum[x])+s[r]-s[x]<<endl;
return m*(x-l+1)-(s[x]-s[l-1])+(l-1)*(sum[x]-sum[l-1])-(r+1)*(sum[r]-sum[x])+s[r]-s[x];
}
int L[200005],R[200005];
vector<int> tmp;
inline bool cmp(int x,int y){
return L[x]<L[y];
}
long long ans[200005];
void cdq(int ql,int qr,int l,int r){
if(ql>qr)return ;
int mid=(ql+qr)>>1,id=tmp[mid],loc=l;
long long res=Get(l,L[id]);
for(int i=l+1;i<=r;i++){
long long T=Get(i,L[id]);
if(T>res)res=T,loc=i;
}
ans[id]=max(ans[id],res);
cdq(ql,mid-1,l,loc);cdq(mid+1,qr,loc,r);
}
vector<int> tree[800005];
void update(int fr,int to,int v,int l=1,int r=n,int i=1){
if(fr>r||to<l)return ;
if(fr<=l&&to>=r){
tree[i].push_back(v);
return ;
}
int mid=(l+r)>>1;
update(fr,to,v,l,mid,i<<1);update(fr,to,v,mid+1,r,i<<1|1);
}
void build(int l=1,int r=n,int i=1){
sort(tree[i].begin(),tree[i].end(),cmp);tmp=tree[i];
cdq(0,tmp.size()-1,l,r);tree[i].clear();
if(l==r)return ;
int mid=(l+r)>>1;
build(l,mid,i<<1);build(mid+1,r,i<<1|1);
}
inline void solve(){
for(int i=1;i<=n;i++){
s[i]=s[i-1]+i*a[i];
sum[i]=sum[i-1]+a[i];
}
for(int i=1;i<=n;i++){
lim[i]=max(1,lim[i-1]-1);
while(lim[i]<i&&i+lim[i]<=n&&sum[i+lim[i]]-sum[i-lim[i]-1]<=m)lim[i]++;
}
// for(int i=1;i<=n;i++)cout<<a[i]<<" ";cout<<endl;
// for(int i=1;i<=n;i++)cout<<i<<" lim "<<lim[i]<<endl;
for(int i=1;i<=q;i++)update(L[i],(L[i]+R[i])>>1,i);
build();
}
int main(){
scanf("%d%lld",&n,&m);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
scanf("%d",&q);
for(int i=1;i<=q;i++)scanf("%d%d",&L[i],&R[i]);
solve();
reverse(a+1,a+n+1);
for(int i=1;i<=q;i++)L[i]=n-L[i]+1;
for(int i=1;i<=q;i++)R[i]=n-R[i]+1;
for(int i=1;i<=q;i++)swap(L[i],R[i]);
solve();
for(int i=1;i<=q;i++)printf("%lld\n",ans[i]);
return 0;
}
AGC049E Increment Decrement
给定正整数 \(n, c(1 \leq c \leq n)\) ,定义一个长度为 \(n\) 的非负整数序列 \(a\) 的代价为如下问题的答案:
当前有一个长度为 \(n\) 的序列 \(x\) ,初始所有 \(x_{i}=0\) ,你可以进行如下两种操作:
-
将一个位置的 \(x_{i}\) 增加 \(1\) 或者减少 \(1\) ,代价为 \(1\) 。
-
选择一个区间,将这个区间的 \(x_{i}\) 整体增加 \(1\) 或者整体减少 \(1\) ,代价为 \(c\) 。
求使得 \(\forall i, x_{i}=a_{i}\) 需要的最小代价。
现在给出 \(n\) 个长度为 \(k\) 的正整数序列 \(b_{1}, \cdots, b_{n}\) 。对于每一个 \(i\) ,从第 \(i\) 个序列 \(b_{i}\) 中选出一个元素作为 \(a_i\) 。可以发现这样能得到 \(n^k\) 种序列,求出这 \(n^k\) 种序列代价的总和,对 \(10^9+7\) 取模。
\(1 \leq n, k \leq 50,1 \leq c \leq n, 1 \leq b_{i, j} \leq 10^{9}\)
\(2 s, 1024 \text{MB}\)
考虑对于确定的 \(a\) 序列,如何求出代价。
只有第一种操作时,答案显然为 \(\sum |a|\) 。
只有第二种操作时,考虑差分,答案为 \(\sum c\times \max(0,a_{i+1}-a_{i})\) 。
考虑将序列 \(a\) 化成两部分 \(a-y\) 和 \(y\) ,第一部分使用第一种操作,第二部分使用第二种操作,代价为:
设 \(dp[i][j]\) 表示处理完 \(1\) 到 \(i\) 这个区间,\(y_i=j\) 的最小代价,有:
考虑把 \(|a_i-j|\)放到 \(i+1\) 处统计,有:
初始化 \(dp[1][j]=c\times \max(j,0)\),答案是 \(dp[n+1][0]\) 。
可以用归纳法证明 \(dp[i,...]\) 是凸函数,首先 \(dp[1,...]\) 是凸函数,然后考虑 \(dp[i−1]\) 向 \(dp[i]\) 转移时,首先加上了一个绝对值函数 \(|a_{i−1}−x|\),然后又和 \(f(x)=c\times \max(x,0)\) 进行了 \(\min\) 卷积,执行这些操作后还是凸函数。
这时候就可以直接上 \(\text{slope trick}\) 了,考虑维护 \(0\) 处的点值 \(v_{0}\) 和描述斜率变化的集合 \(S\) ,对于添加 \(\left|a_{i-1}-x\right|\) 这个绝对值函数,我们首先把 \(v_{0}\) 加上 \(a_{i-1}\) ,然后向 \(S\) 中添加两个 \(a_{i-1}\) 处的转折点。
稍微麻烦一下的操作是和 \(f(x)\) 进行 \(\min\) 卷积。回忆两个凸函数是如何进行 \(\min\) 卷积的,实际上就是类似归并排序,选择 添加增量少的部分。效果如下图,一句话概括就是掐头去尾:
我们取出最小的转折点 \(a\) 和最大的转折点 \(b\),首先把 \(v_0\) 减去 \(a\),从 \(S\) 中删除 \(a\) 就可以使得开头段的斜率变为 \(0\) 。然后从 \(S\) 中删除 \(b\) 就可以使得结尾段的斜率变为 \(c\) 。
然后考虑原来的问题,对于所有的 \(a_i\in \{b\}_i\),求 \(a\) 序列代价的总和。
首先,对于加上去的部分,答案是好求的,那就是 \(\sum_{a_i\in \{b\}_i}\sum_{i=1}^n a_i\) ,也就是 \(\sum b\times k^{n-1}\) 。
考虑怎么算减去的部分,我们显然不能将当前的所有拐点记下来,但是我们可以将 \(b\) 离散化,每次枚举一个值 \(b_i\),然后将大于 \(b\) 的数设成 \(0\) ,小于的设成 \(1\) ,然后 \(dp\) 求出所有方案中,一共弹出了多少个 \(0\) ,将这个数乘上 \((b_i-b_{i-1})\) 就是这个值的贡献。
设 \(f[i][j]\) 表示前 \(i\) 次操作,现在 \(S\) 中有 \(j\) 个 \(0\) 的方案数,\(g[i][j]\) 表示对应取出 \(1\) 的总个数。
转移就暴力添加 \(0/1\),模拟 \(\text{slope trick}\) 的过程即可,时间复杂度 \(O(nk⋅nc)=O(n^2kc)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int md=1e9+7;
int n,c,k,ans,sub,h[105],f[105][105],g[105][105];
pair<int,int> b[10005];
int main(){
scanf("%d%d%d",&n,&c,&k);
for(int i=1;i<=n;i++) for(int j=1;j<=k;j++){
int x;scanf("%d",&x);ans=(ans+x)%md;
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=1ll*f[i-1][j]*vl%md,f2=1ll*g[i-1][j]*vl%md;
if(tj)tj--;
else f2=(f2+f1)%md;
tj=min(tj,c);
f[i][tj]=(f[i][tj]+f1)%md;g[i][tj]=(g[i][tj]+f2)%md;
}
for(int i=0;i<=n;i++)sub=(sub+1ll*g[n][i]*(b[w].first-b[w-1].first))%md;
h[b[w].second]++;
}
for(int i=1;i<n;i++)ans=1ll*ans*k%md;
printf("%d\n",(ans-sub+md)%md);
return 0;
}
ddtt / gym 102759C Economic One-way Roads
给一个 \(n\) 个点的简单无向图,你需要将每条边定向,对于每条边 \((i, j)\) ,将这条边定向为 \(i \rightarrow j\) 及 \(j \rightarrow i\) 分别有一个代价。
你需要使得定向后图强伡通,且最小化定向的总代价。输出最小总代价或输出无解。
\(n \leq 18\)
\(5 s, 1024 \text{MB}\)
问题可以看成,选择图的一些边,将它们定向使图为强连通图,没有用到的边选择代价最小的方式。因此可以经过一些处理,将问题变为选择一些边定向使得这些边构成强连通图,将一条边定向为两种方向 分别有一个代价,不定向的边没有代价。
考虑如何描述一个强连通图。可以得到如下方式:
选择一个环作为初始的强连通分量,接下来每次选择一条链 \(s \rightarrow v_{1} \rightarrow \cdots \rightarrow t\) ,满足 \(s, t\) 在强连通分量中而剩余的点当前不在强连通分量中,然后将这条链加入强连通分量。
可以证明,对于任意一个强连通图,可以使用这种方式找到边集的一个子集,使得只考虑子集中的边,所有点强连通。
证明: 显然强连通图存在一个环,因此第一步可以做到。考虑接下来的每一步,设当前已经加入的点集为 \(S\) ,剩余点集为 \(T\) ,则一定存在一个环,这个环包含至少一个 \(S\) 中点和至少一个 \(T\) 中点(否则图不是强连通),一定可以在这个环上选择一段加入。
那么可以设 \(f_{S}\) 表示当前加入的集合为 \(S\) 时,内部需要的最小代价。初始找环可以使用状压 \(\mathrm{dp}\) \(O\left(n^{2} 2^{n}\right)\) 解决。
但直接枚举加入的链的点集不能接受,考虑将加入链的过程再写成一个 \(dp\) 。
设当前状态为 \(f_{S}\) ,考虑枚举下一条加入的链的起点 \(s\) 和终点 \(t\) 。考虑走一条 \(s\) 到 \(t\) 的链的过程,可以设 \(dp_{S, x, t}\) 表示当前经过的点集合为 \(S\),当前走到了 \(x\),链的终点为 \(t\) 时的最小代价,然后钦定除了最后一步外不能走已经经过的点(否则可能一条边两个方向都被用),走到 \(t\) 后用对应状态更新 \(f_{S}\) 。复杂度 $O\left(n^{3} 2^{n}\right) $ 。
但还有一个小问题。可能出现 \(s \rightarrow x \rightarrow s\) 的情况,而这显然是不行的。为了避免这种情况,可以在加入起点终点相同的路径时,枚举走的前两步。可以发现这样不会增加复杂度。
复杂度 \(O\left(2^{n} n^{3}\right)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int dp[1<<18][18][18][2],f[1<<18],G[18][18];
inline void Upt(int &x,const int v){x=min(x,v);}
int main(){
scanf("%d",&n);
int ans=0,lim=1e9;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++)scanf("%d",&G[i][j]);
}
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
if(G[i][j]<0) continue;
int t=min(G[i][j],G[j][i]);
ans+=t,G[i][j]-=t,G[j][i]-=t;
lim+=t+G[i][j]+G[j][i];
}
}
int all=(1<<n)-1;
memset(f,0x3f,sizeof(f));
memset(dp,0x3f,sizeof(dp));
f[1]=0;
for(int S=1;S<=all;S++){
for(int u=0;u<n;u++){
for(int v=0;v<n;v++){
for(int b=0;b<=1;b++){
if(dp[S][u][v][b]>lim)continue;
for(int T=all^S;T;T&=T-1){
int w=__builtin_ctz(T);
if(G[u][w]>=0)Upt(dp[S|(1<<w)][w][v][1],dp[S][u][v][b]+G[u][w]);
}
if(b&&G[u][v]>=0)Upt(f[S],dp[S][u][v][b]+G[u][v]);
}
}
}
for(int u=0;u<n;u++)if(S>>u&1){
for(int v=0;v<n;v++)if(S>>v&1){
for(int w=0;w<n;w++)if(!(S>>w&1)){
if(G[u][w]<0)continue;
Upt(dp[S|(1<<w)][w][v][u!=v],f[S]+G[u][w]);
}
}
}
}
if(f[all]>lim)puts("-1");
else printf("%d\n",ans+f[all]);
return 0;
}
ARC113F Social Distance
给一个长度为 \(n+1\) 的正整数序列 \(x\),保证 \(x_{0}=0\) 且 \(x\) 递增。
现在随机生成一个长度为 \(n\) 的实数序列 \(y\),其中 \(y_{i}\) 在 \(\left[x_{i-1}, x_{i}\right]\) 间随机生成。 定义序列 \(y\) 的权值为 \(\min _{i=1}^{n-1}\left(y_{i}-y_{i-1}\right)\),求 \(y\) 权值的期望,模 \(998244353\) 。
\(n \leq 20\)
\(4 s, 1024 \text{MB}\)
设 \(f(v)\) 表示权值大于等于 \(v\) 的概率,则可以发现答案的期望为 \(\int_{v=0}^{+\infty} f(v) dv\) 。这一点可以使用分部积分直接解释。
考虑如何计算一个 \(f(v)\) ,问题相当于 \(y_{i}\) 在 \(\left[x_{i-1}, x_{i}\right]\) 中随机生成,求 \(\forall i, y_{i}+v \leq y_{i+1}\) 的概率.考虑令 \(b_{i}=y_{i}-i * v\) ,则相当于 \(b_{i}\) 在 \(\left[x_{i-1}-i v, x_{i}-i v\right]\) 中随机生成,求 \(\forall i, b_{i} \leq b_{i+1}\) 的概率。
考虑将概率乘上 \(\prod_{i}\left(x_{i}-x_{i-1}\right)\) ,这相当于将 \(b_{i}\) 在 \(\left[x_{i-1}-i v, x_{i}-i v\right]\) 出现的概率分布都是 \(1\) 。
按照所有的区间端点将值域划分成 \(2 k\) 段,按照值从小到大排序,考虑枚举每个点的取值属于哪一段,则值所在的段编号一定不降。考虑一段内的情况,设一段长度为 \(l\) ,且有 \(k\) 个数取值在这一段内。可以考虑设 \(dp_{i, j, k}\) 表示考虑了前 \(i\) 个元素,当前第 \(i\) 个元素的值在第 \(j\) 段内,前面一共有 \(k\) 个元素在这一段内时,前面的所有情况贡献和。这样可以 \(O\left(n^{3}\right)\) 求出一个 \(f(v)\) ,这里还有很多不同的 \(dp\) 方式。
考虑 \(v\) 变化的情况,此时可以将所有边界看成关于 \(v\) 的一次函数,因此如果 \(v \in[l, r]\) 时,所有端点间的大小顺序不发生改变,则这一段内的 \(\mathrm{dp}\) 转移相同。在 \(\mathrm{dp}\) 时维护关于 \(v\) 的多项式,可以发现单次复杂度变为 \(O\left(n^{4}\right)\) 。
同时因为所有边界都是一次函数,可以发现大小顺序发生改变的次数为 \(O\left(n^{2}\right)\) ,因此对于每一段分别求贡献即可。
复杂度 \(O\left(n^{6}\right)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
const int md=998244353;
inline int pwr(int x,int y){
int res=1;
while(y){
if(y&1)res=1ll*res*x%md;
x=1ll*x*x%md;y>>=1;
}
return res;
}
struct frac{
int m,d;
frac(int _m,int _d){
m=_m;d=_d;
}
inline bool operator==(const frac &b)const{
return m==b.m&&d==b.d;
}
inline bool operator<(const frac &b)const{
return m*b.d<b.m*d;
}
inline operator int(){
return 1ll*m*pwr(d,md-2)%md;
}
};
struct point{
int b,k,id;
point(int _b,int _k,int _id){
b=_b;k=_k;id=_id;
}
inline frac operator()(const frac &y)const{
return frac(b*y.d-k*y.m,y.d);
}
};
int x[25],w[55],inv[25],ifac[25];
inline void init(){
inv[0]=inv[1]=ifac[0]=ifac[1]=1;
for(int i=2;i<=n+1;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n+1;i++)ifac[i]=1ll*ifac[i-1]*inv[i]%md;
}
typedef vector<int> poly;
poly operator+(poly a,poly b){
if(a.size()<b.size())swap(a,b);
poly c=a;
for(int i=0;i<b.size();i++)c[i]=(c[i]+b[i])%md;
return c;
}
poly operator*(const poly& a,const poly& b){
poly c(a.size()+b.size()-1);
for(int i=0;i<a.size();i++)
for(int j=0;j<b.size();j++)c[i+j]=(c[i+j]+1ll*a[i]*b[j]%md)%md;
return c;
}
poly operator*(const poly& a,int b){
poly c(a.size());
for(int i=0;i<a.size();i++)c[i]=1ll*a[i]*b%md;
return c;
}
poly integral(const poly& a){
poly c(a.size()+1);
for(int i=1;i<c.size();i++)c[i]=1ll*a[i-1]*inv[i]%md;
return c;
}
int getval(const poly& a,int x){
int x2=1,res=0;
for(int i=0;i<a.size();i++)res=(res+1ll*a[i]*x2%md)%md,x2=1ll*x2*x%md;
return res;
}
poly f[55][25];
int main(){
scanf("%d",&n);init();
for(int i=0;i<=n;i++)scanf("%d",&x[i]);
vector<point>p;
for(int i=1;i<=n;i++)p.emplace_back(x[i-1],i-1,2*i-1),p.emplace_back(x[i],i-1,2*i);
vector<frac>t;
for(int i=0;i<2*n;i++)
for(int j=i+2-(i & 1);j<2*n;j++)t.emplace_back(p[j].b-p[i].b,p[j].k-p[i].k);
sort(t.begin(),t.end()),t.erase(unique(t.begin(),t.end()),t.end());
int ans=0;
for(int nt=0;nt<t.size()-1;nt++){
auto cmp=[&](const point& a,const point& b){
if(a(t[nt])==b(t[nt]))return a.k>b.k;
return a(t[nt])<b(t[nt]);
};
sort(p.begin(),p.end(),cmp);
for(int i=0;i<p.size();i++)w[p[i].id]=i;
f[0][0]={1};
for(int i=1;i<p.size();i++){
for(int j=0;j<=n;j++){
f[i][j].clear();
poly z={(p[i].b-p[i-1].b+md)%md,(p[i-1].k-p[i].k+md)%md},z2=z;
for(int k=1;k<=j;k++){
int nid=j-k+1,L=w[2*nid-1],R=w[2*nid]-1;
if(L>i-1||i-1>R)break;
f[i][j]=f[i][j]+f[i-1][nid-1]*z*ifac[k];
z=z*z2;
}
f[i][j]=f[i][j]+f[i-1][j];
}
}
poly g=integral(f[p.size()-1][n]);
ans=(ans+(getval(g,t[nt+1])-getval(g,t[nt])+md)%md)%md;
}
for(int i=1;i<=n;i++)ans=1ll*ans*pwr(x[i]-x[i-1],md-2)%md;
printf("%d",ans);
return 0;
}