小清新简单dp题(三)
17. P3411 序列变换
竟然自己想出来了正解!!!!1
很容易想到一个结论:\(n\) 次操作之内必然可以使序列变为不降。
显然可以做到:只需要从小到大将每一个元素依次移动到数列结尾即可。
这个结论有什么用呢?没什么用,不过我们可以顺着继续向下想。
如果将第 \(n\) 大的元素移动到数列末尾,在最优情况下,是不是就意味着不会再移动任何数到数列末尾了?否则一定还需要将该元素再移动到末尾一次,显然不优。
同理,如果将第 \(n-1\) 大的元素移动到数列末尾,那么会且仅会将第 \(n\) 大的元素移动到末尾。移动到开头的规则也同理。
而剩下的是哪些元素呢?显然是一段大小连续且上升的元素。
所以我们就成功地将题目转化为了:求出序列中的最长无缝不下降子序列。
这个就很好求了,不妨设 \(dp_i\) 表示当前以大小为 \(i\) 的元素作为结尾的最长无缝不下降子序列,如果前面已经出现了 \(i\),则:
如果 \(i\) 从来没出现过,则有:
注意需要提前将原数组离散化。答案即为序列长度减去 \(dp\) 数组每一项的最大值。
for (int i=1;i<=n;i++){
if (!dp[a[i]]) dp[a[i]]=dp[a[i]-1]+1;
else dp[a[i]]++;
ans=max(dp[a[i]],ans);
}
update:假了,但是题解就是这么写的,懒得更新((
18. P3486 [POI2009]KON-Ticket Inspector
数据结构优化 dp。
注意到,题目中询问的是不同的人的最多票数。
考虑设 \(dp_{i,k}\) 表示前 \(i\) 个车站检票 \(k\) 次且要在车站 \(i\) 检票时最多能检到的不同的人的票数。发现其实每个人的上下车轨迹其实都是一段区间,而当我们从决策 \(j\) 转移过来的时候,相当于第 \(k\) 次检票会检到从区间 \([j+1,i]\) 上车,从区间 \([i+1,n]\) 下车的人的个数。
那么本题就很明了了。注意到暴力枚举人数复杂度会很高,于是采用二维前缀和优化即可。
时间复杂度 \(O(n^2k)\)。
for (int i=0;i<=n;i++)
dp[i][0]=0;
for (int i=1;i<=n;i++)
for (int j=0;j<i;j++)
for (int s=1;s<=k;s++){
int next=dp[j][s-1]+sum[i][n]-sum[j][n]-sum[i][i]+sum[j][i];
if (next>dp[i][s]){
dp[i][s]=next;
f[i][s]=j;
}
}
19. P2079 烛光晚餐
套路背包题。
注意到其实本题有三个维度,小明的喜爱程度、小红的喜爱程度以及体积。
其中小明的喜爱程度与体积是限制维,而小红的喜爱程度是优化维。
于是考虑将小明的喜爱程度和体积作为下标,将小红的喜爱程度作为内容,进行0-1背包转移。
最后输出所有小明喜爱程度大于 \(0\) 的 dp 最大值即可。
注意小明的喜爱程度可能为负数,需要将其整体向右位移 \(500\) 作为真正下标。
dp[0][500]=0;
for (int i=1;i<=n;i++){
int c=read(),x=read(),y=read();
for (int j=v;j>=c;j--)
for (int k=1000;k>=x;k--)
dp[j][k]=max(dp[j][k],dp[j-c][k-x]+y);
}
int ans=-1;
for (int i=1;i<=v;i++)
for (int j=500;j<=1000;j++)
ans=max(ans,dp[i][j]);
20. P2606 [ZJOI2010]排列计数
将所有的 \(i\) 向 \(i\times 2\) 和 \(i\times 2+1\) 连边,发现构成了一棵树。
根据题目中的条件,树的祖先在排列中的位置要先于后代。
于是就可以转化为一个树形的组合计数问题。对于以 \(i\) 为根的一棵子树,\(i\) 自身分配的排列序号一定是最小的,左子树会取以 \(i\) 为根的子树内 \(size_{lson_i}\) 个编号,共有 \(C_{size_{lson_i}+size_{rson_i}}^{size_{lson_i}}\) 种可能。左子树取完后右子树得到取值自然就固定下来了。
而左右子树内部又会再次分配取值,所以 \(dp_{i}=dp_{lson_i}\times dp_{rson_i} \times C\).
不过注意到 \(m\) 不一定小于 \(n\),所以不能直接求组合数,需要使用 Lucas 定理解决。
ll C(ll x,ll y){
if (y>x) return 0;
return jc[x]*ksm(jc[y],m-2,m)%m*ksm(jc[x-y],m-2,m)%m;
}
ll lucas(ll x,ll y){
if (y==0) return 1;
return C(x%m,y%m)*lucas(x/m,y/m)%m;
}
ll solve(ll x){
if (x>n) return 1;
ll ans=solve(x<<1)*solve(x<<1|1)%m*lucas(size[x<<1]+size[x<<1|1],size[x<<1])%m;
return ans;
}
21. P2625 豪华游轮
可以将操作分类,注意到开始的角度向前走一定是最优的,所以可以先将所有向前走的步数在一开始的时候走完。
然后贪心地来看,我们需要通过旋转找到一个最优秀的角度,然后一次性将所有后退的步数走完,如果还有角度剩余,则放到最后转即可。
找最适合后退的角度的过程相当于什么?没错,可以转化为一个背包问题。
设 \(dp_{i,j}\) 表示对于前 \(i\) 个旋转是否能转到角度 \(j\),然后进行转移即可。
注意最后需要将角度值转化为弧度制,然后用三角函数公式算出最终移动的距离。
最终的计算代码如下:
#define pai acos(-1)//acos为cos的逆运算,cosΠ=-1
for (int i=0;i<=180;i++){
if (dp[m][180+i]||dp[m][180-i]){
double d=180-i;
printf("%.6lf\n",sqrt(ans*ans+ba*ba-2*ans*ba*cos(d*pai/180)));
return 0;
}
}
22. P3694 邦邦的大合唱站队
\(n\) 很大,\(m\) 很小,考虑将 \(m\) 状压。
注意到每一个乐队的成员一定是连续的一段,所以如果我们将乐队之间的排队顺序确定下来,我们就可以确定下来整个序列的样子。
由于是求顺序,且 \(m\) 很小,于是可以将已经选择的乐队状压。设 \(dp_S\) 表示已经排好的乐队集合为 \(S\),所需要交换的人数的最小值。
预处理出每个乐队人数的前缀和,然后枚举集合中包含的乐队进行转移即可。
dp[0]=0;
for (int i=1;i<(1<<m);i++){
int now=solve(i);
for (int j=1;j<=m;j++){
if (i&(1<<(j-1))){
int last=now-js[j];
dp[i]=min(dp[i],dp[i^(1<<(j-1))]+now-last-sum[j][now]+sum[j][last]);
}
}
}
23. P2051 [AHOI2009] 中国象棋
标签状压 dp 是认真的吗???好吧也勉强算是状压。
题目相当于是让求出每一行象棋个数不超过 \(2\),每一列个数也不超过 \(2\) 的方案个数。
正常的状压可以用 \(dp_{i,S}\) 表示前 \(i\) 列中有一个炮或两个炮的列的集合为 \(S\) 的方案个数,可以过掉 \(50\%\) 的数据。
但我们发现这种状压方式很弱,弱在哪呢?这种状压方式实际上是多维护了某一列有几个炮的。但事实上,这并不是我们需要关心的内容。转移时,我们只关心目前为止有几个位置有一个炮,几个位置有两个炮,而不是哪几个位置。
所以我们可以进一步优化状态,设 \(dp_{i,j,k}\) 表示对于前 \(i\) 行,其中有 \(j\) 列有一个炮,\(k\) 列有两个炮的方案数,然后根据前一行的 dp 值处理组合数正常转移即可。
dp[0][0][0]=1;
for (int i=1;i<=n;i++){
for (int j=0;j<=m;j++){
for (int k=0;k<=m;k++){
dp[i][j][k]=dp[i-1][j][k];
if (j>=2) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-2][k]*C(m-k-j+2,2)%mod)%mod;//0->1 0->1
if (k>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j][k-1]*C(m-j-k+1,1)%mod*C(j,1)%mod)%mod;//0->1 1->2
if (k>=2) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+2][k-2]*C(j+2,2)%mod)%mod;//1->2 1->2
if (k>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+1][k-1]*C(j+1,1)%mod)%mod;//1->2
if (j>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k]*C(m-k-j+1,1)%mod)%mod;//0->1
}
}
}
24. P4141 消失之物
非常常见的 trick。
首先很容易想到最暴力的背包解法:对于每 \(n-1\) 个物品跑一遍 0-1 背包,然后依次输出答案。时间复杂度 \(O(n^2m)\).
这样很显然是不优的——对于每个物品我们都重复进行了 dp,浪费了许多效率。于是考虑优化。
可以联想到一个经典问题:求区间最大双段子段和。
我们都会求普通的最大子段和,dp 或者前缀和处理等做法都可以将它完美解决。但是扩展到双段问题似乎就变得有些棘手——对于 dp,状态的设计将会变得十分复杂繁琐。
考虑一种更优美的解法,即从前到后和从后到前分别跑一遍 dp,求出前 \(i\) 项及后 \(i\) 项的最大子段和,然后枚举双段子段和的断点,将两遍 \(dp\) 结果拼凑起来取最大值即可。
这个解法看上去十分具有启发性。扩展的来看,如果我们可以通过从前到后递推出前 \(i\) 项的 dp 值,那么我们同样也可以求出后 \(i\) 项的 dp 值,将两者拼凑到一起则可以求出扩展到双段的相同问题的 dp 值。
再回来看本题。可以发现去除掉第 \(i\) 个物品的背包相当于就是前 \(i-1\) 项以及后 \(n-i\) 项背包拼凑到一起,不过此时我们还缺少背包的体积信息。所以可以考虑先正反分别跑一遍完整的背包,然后枚举断点和左侧体积,顺便推出右侧体积,将二者相乘后相加就可以求出每一个 cnt 的值。
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
int ans=0;
for (int k=0;k<=j;k++)
ans=(ans+dp[0][i-1][k]*dp[1][i+1][j-k]%10)%10;
printf("%d",ans);
}
puts("");
}
然而这个做法并没有怎么优化复杂度。。。于是就假了
考虑正解。计数背包在求解的过程中,对于体积为 \(m\) 时的物品 \(i\) ,贡献其实为 \(dp_{m-w_i}\),所以我们在删除第 \(i\) 个物品时减去这个贡献即可。
不过要注意,在计算贡献的时候我们是倒序枚举的,所以在删除贡献的时候我们应该正序枚举。
25. CF1575L Longest Array Deconstruction
稍微简化一下题面,原题其实就是让你将 \(A\) 数组删去若干数得到 \(B\) 数组,使得 \(B\) 数组满足 \(a_i=i\) 的项数尽可能的多。
很容易想到设 \(dp_{i,j}\) 表示当前处理到第 \(i\) 个数,且前面一共删掉了 \(j\) 个数时最大的满足条件的项数。则有:
时空复杂度显然不行,让我们再优化。
我们发现对答案有贡献的条件只有一个,也就是 \(a_i+j=i\)。所以其实我们的第二维的条件是有点浪费的。
考虑设 \(dp_i\) 表示对于前 \(i\) 个数,第 \(i\) 个数被放在 \(a_i\) 的位置上时的最大满足条件数(\(a_i\le i\)),此时我们有转移方程:
于是就是喜闻乐见的最长上升子序列的问题了。
26. CF1442D Sum
由于 \(n\) 个数组不降,我们可以推出一个结论:我们至多只会有一个序列未取完;剩下的序列要不一个不取,要不全部取完。
这个结论很容易通过贪心+反证的方式进行证明,在此不再赘述。
然后我们就将该问题转化成了:
将 \(n-1\) 个完整物品和一个部分物品放入背包,所能得到的最大价值。
乍一看此题和 T24 差不多,都是有一个物品受到影响,\(n-1\) 个物品不受影响。
但仔细观察发现,T24 是计数类背包,加法具有可逆性;而本题却是普通的 0-1 背包,最大值的操作并不具有可逆性。于是 T24 的解法就泡汤了。
既然不具有可逆性,那么我们就只能考虑将 \(n-1\) 个物品的答案拼起来;观察数据范围,这个拼起来的过程复杂度只能是 \(n\log n\) 级别的,毕竟我们还需要 \(O(n)\) 的时间来枚举剩下的那个物品的选择长度。
于是考虑分治。类似于线段树,我们可以用均摊 \(\log n\) 级别的复杂度来拼凑出选择某个物品作为部分物品时的最优答案。
具体来说,对于一个区间 \([l,r]\),考虑得到中点 \(mid\) 后,将编号在区间 \([mid+1,r]\) 的物品加入背包,然后递归解决区间 \([l,mid]\);回溯之后,将 \(dp\) 数组恢复到之前的状态,然后将区间 \([l,mid]\) 的物品加入背包,再递归解决区间 \([mid+1,r]\)。当左右端点相等时,枚举当前物品选择的部分,与背包中剩余体积的最大价值相加后取最大值即可。
注意元素个数的规模是 \(1e6\) 级别的,并不好直接储存;不过某个数组中 \(k\) 项之后的元素其实都是没有用的,所以我们在输入时将每个数组的长度与 \(k\) 取最小值,读到 \(k\) 项之后的元素不存入数组即可。
void solve(int o,int l,int r){
if (l==r){
for (int i=1;i<=min(p[l],k);i++)
ans=max(ans,sum[l][i]+dp[k-i]);
return;
}
for (int i=0;i<=k;i++) zc[o][i]=dp[i];
int mid=l+r>>1;
for (int i=l;i<=mid;i++)
for (int j=k;j>=p[i];j--)
dp[j]=max(dp[j],dp[j-p[i]]+sum[i][p[i]]);
solve(o+1,mid+1,r);
for (int i=0;i<=k;i++) dp[i]=zc[o][i];
for (int i=mid+1;i<=r;i++)
for (int j=k;j>=p[i];j--)
dp[j]=max(dp[j],dp[j-p[i]]+sum[i][p[i]]);
solve(o+1,l,mid);
}