day 4 DP 选讲
DP 选讲
T1 CF258D Little Elephant and Broken Sorting
一个长度为 n 的整数排列,每次操作给出两个位置,有一半的概率交换两数,有一半概率保持不变,求 m 次操作结束后数列的期望逆序对数量。\(n,m\le 1000\)
和的期望等于期望的和,逆序对的期望数量就是每两个数 \(i<j\) 是否构成逆序对的概率和。
记 \(dp(i,j)\) 为 i,j 构成逆序对的概率。
注意到每次操作如果交换的是 \(x,y\),那么一定有 \(dp(x,y)=dp(y,x)=0.5\),因为交换是随机的。
考虑每次之后如何转移,不难发现对于 \(dp(x,i)\) 和 \(dp(y,i)\),分别计算交换、不交换的最终期望分别为 \(dp(x,i),dp(y,i)\),相加除以二即可,\(dp(i,x),dp(i,y)\) 同理,最终转移方程为:
f[x][i]=f[y][i]=(f[x][i]+f[y][i])*0.5;
f[i][x]=f[i][y]=(f[i][x]+f[i][y])*0.5;
初始化时枚举所有的 \(i,j\),若 \(i<j\) 且 \(a_i>a_j\),则初始时概率为 1,否则为 0,答案为 \(\sum_{i<j}f(i,j)\)。
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[1005];
double f[1005][1005];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(a[i]>a[j])f[i][j]=1;
}
}
while(m--){
int x,y;
cin>>x>>y;
for(int i=1;i<=n;i++){
f[x][i]=f[y][i]=(f[x][i]+f[y][i])*0.5;
f[i][x]=f[i][y]=(f[i][x]+f[i][y])*0.5;
}
f[x][y]=f[y][x]=0.5;
}
double ans=0;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
ans+=f[i][j];
}
}
printf("%.10f",ans);
return 0;
}
T2 P2519 [HAOI2011] problem a
一次考试共有 \(n\) 个人参加,可能出现多个人成绩相同的情况。第 \(i\) 个人说:“有 \(a_i\) 个人成绩比我高,\(b_i\) 个人成绩比我低。”
请求出最少有几个人没有说真话。\(n\le10^5\)
考虑对问题进行转化,“有 \(a_i\) 个人成绩比我高,\(b_i\) 个人成绩比我低。”,可以转化为他的排名为 第 \(a_i+1\)名,有 \(n-a_i-b_i\) 个人和他并列在这个分数,所以这个人的话就可以翻译成有区间 \([a_i+1,n-b_i]\) 的这些人分数是一样的。
每个人都说这样一个区间,显然如果一些人的话是真的,这些人的区间不能有重叠关系、包含关系,因为如果有,那么他们描述的一个分数同分的人不一样。
考虑如何 DP 求出不相交区间数量的最大值。
记 \(f[i]\) 为前 i 名最多有多少人说真话,把区间按 \(r_i\) 排序,对于相同 \(r_i\),枚举所有以 i 为右端点区间来更新 \(f_i\),转移方程:\(f[i]=max(f[i],f[p]+min(r-l+1,mp[a[j]]))\),注意每个区间说话的人最大 \(r-l+1\),否则是不合法的,细节上可以用 map 记录每个区间的出现次数。
#include<bits/stdc++.h>
using namespace std;
int n;
struct node{
int x,y;
bool operator < (const node &o) const{
return x != o.x ? x < o.x : y < o.y;
}
}a[100005];
bool cmp(node x,node y){
return x.y<y.y;
}
map<node,int>mp;
int f[100005],tot;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
int ai,bi;
cin>>ai>>bi;
if(n-ai-bi<=0){
continue;
}
tot++;
a[tot].x=ai+1;
a[tot].y=n-bi;
mp[a[tot]]++;
}
sort(a+1,a+tot+1,cmp);
int p=0;
for(int i=1;i<=tot;i++){
f[i]=f[i-1];
while(p<i&&a[p+1].y<a[i].x)p++;
for(int j=i;j<=tot&&a[j].y==a[i].y;j++){
int l=a[j].x,r=a[j].y;
f[i]=max(f[i],f[p]+min(r-l+1,mp[a[j]]));
}
if(i<tot&&a[i+1].y==a[i].y)continue;
}
cout<<n-f[tot];
return 0;
}
T4 P7962 [NOIP2021] 方差
对长度为 n 的序列 a,进行任意次以下操作:选择一个数 \(a_i\),将其变为 \(a_{i-1}+a_{i+1}-a_i\),求操作后方差最小值的 \(n^2\) 倍,\(n\le 10^4\)。
测试点编号 n≤ ai≤ 1∼3 4 10 4∼5 10 40 6∼8 15 20 9∼12 20 300 13∼15 50 70 16∼18 100 40 19∼22 400 600 23∼25 \(10^4\) 50
方差的 \(n^2\) 倍可以拆成
我们通过打表,可以得知方差的差分数组是单谷的,即查分数组先减少再增加,所有我们可以对差分数组进行排序,遍历一次,每次把新加入的数插入到两侧,因为这是目前为止最大的数。
考虑如何DP。设 \(f(i,x)\) 为已经考虑完前 \(i-1\) 个差分值,当 a 数组方程最小时,x 为此时 a 数组的和,此时的平方和为 \(f(i,x)\),记录 x 是因为方差式子中有平方项,需用到二倍乘积。
每次转移,需要转移 \(d_i\) 放左边和放右边两种情况。
- 放左边
当前数列为:
把 \(d_i\) 作为第一个差分插入到最前面,新数列变为:
新和: 原来每个非零元素 \(a_2,\dots,a_i\) 都多了 \(d_i\),同时新增了一个单独的 \(d_i\),总共多了 \(i \cdot d_i\),所以
新平方和: 原来平方和是 \(\sum a_k^2\),现在新平方和 =
展开:
而 \(\sum_{k=1}^{i} a_k = x\),所以增加量为:
因此新平方和:
这就是放左边的转移:
- 放右边
当前数列仍然是 \(0, a_2, \dots, a_i\)。
把 \(d_i\) 作为最后一个差分追加到末尾,新数列为:
原来的最后一个元素 \(a_i\) 等于前面所有差分之和,也就是 \(s_{i-1}\)。
所以新增加的元素是:
新和: 增加了一个 \(s_i\),所以:
新平方和: 只增加了一个 \(s_i^2\),所以:
这就是放右边的转移:
此时时间复杂度 \(O(n^2a)\),考虑如何优化。
现在只有测试点 23~25 过不去,但是发现 \(a_i\le 50\),所以开始分类讨论。
我们发现当 \(d_i=0\) 时不会发生任何转移,测试的 23~25 中这一情况会有很多,我们直接跳过就行,时间复杂度 \(O(\min\{n,a\}na)\),可以通过本题。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF = 1e18;
int n,a[10005],d[10005],s[10005];
int f[500005];
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=2;i<=n;i++)d[i-1]=a[i]-a[i-1];
sort(d+1,d+n);
s[0]=0;
for(int i=1;i<=n;i++)s[i]=d[i]+s[i-1];
for(int i=0;i<500005;i++)f[i]=INF;
f[0]=0;
int mx=0;
for(int i=1;i<=n;i++){
if(d[i]==0)continue;
for(int j=mx;j>=0;j--){
if(f[j]==INF)continue;
int nxt=j+i*d[i];
if(nxt<500005){
f[nxt]=min(f[nxt],f[j]+2*j*d[i]+i*d[i]*d[i]);
mx=max(mx,nxt);
}
nxt=j+s[i];
if(nxt<500005){
f[nxt]=min(f[nxt],f[j]+s[i]*s[i]);
mx=max(mx,nxt);
}
f[j]=INF;
}
}
int ans=INF;
for(int i=0;i<=mx;i++){
if(f[i]<INF)ans=min(ans,n*f[i]-i*i);
}
cout<<ans;
return 0;
}
T5 P6772 [NOI2020] 美食家
有
n个点,m条带权有向边(边权 ∈[1,5]),从点1出发,走恰好T天回到点1,每个点每次到达都给固定愉悦值,途中某些特定时间在特定点有额外奖励,求最大总愉悦值,若无法回到1则输出 -1。\(n\le 50,m\le 501,T\le 10^9\)
考虑 T 较小时如何 DP,记 \(f[x][i]\) 表示在地 i 天位于节点 x 能获得的最大愉悦值,每次转移枚举出边 \((x,y,w)\),那么 \(f[x][i]\) 转移到 \(f[y][i+w]\) 就需要加上\(c[y]+g[i+w][y]\),其中 g 是美食节带来的额外愉悦值。-------------------------------40分
但是当 T 很大时,我们需要考虑优化,发现 n 很小,容易想到矩阵。先不考虑美食节,这个转移就等价于 max-plus 矩阵乘法:
类似普通矩阵乘法一样,这个式子具有结合律,所以我们可以直接重载乘法,然后用快速幂计算。
但是矩阵乘法只能从 \(f[i]->f[i+1]\),无法直接加 w,但是因为 w 很小,我们考虑拆点。把一个点拆成 5 个点,\(u_1,u_2,..,u_5\),对于一条边 \((u,v,w)\),连边 \(u_w\) 和 \(v_1\) 即可。
在这上面走矩阵乘法,复杂度 \(O(k(5n)^3\log T)\)。-------------------------------65分
对于 k 个美食节,发现两个美食节不能在同一天,所以我们把美食节按照举办时间排序,然后在任意两个美食节之间做一次普通的 DP 转移,转移后加上美食节的额外贡献即可。-------------------------------75分
考虑如何优化,我们可以把矩阵的 2 的多少次方次幂都预处理出来,每次转移时进行二进制分解,就能倍增地进行转移,这和 day 3 模拟赛 T3 的做法很像,在此就不做赘述了。
总时间复杂度 \(O((5n)^3\log T+k(5n)^2\log T)\)。-------------------------------100分
浙公网安备 33010602011771号