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\) 倍可以拆成

\[n\sum_{i=1}^na_i^2-(\sum_{i=1}^na_i)^2 \]

我们通过打表,可以得知方差的差分数组是单谷的,即查分数组先减少再增加,所有我们可以对差分数组进行排序,遍历一次,每次把新加入的数插入到两侧,因为这是目前为止最大的数。

考虑如何DP。设 \(f(i,x)\) 为已经考虑完前 \(i-1\) 个差分值,当 a 数组方程最小时,x 为此时 a 数组的和,此时的平方和为 \(f(i,x)\),记录 x 是因为方差式子中有平方项,需用到二倍乘积。

每次转移,需要转移 \(d_i\) 放左边和放右边两种情况。

  • 放左边

当前数列为:

\[0,\ a_2,\ a_3,\ \dots,\ a_i \]

\(d_i\) 作为第一个差分插入到最前面,新数列变为:

\[0,\ d_i,\ d_i+a_2,\ d_i+a_3,\ \dots,\ d_i+a_i \]

新和: 原来每个非零元素 \(a_2,\dots,a_i\) 都多了 \(d_i\),同时新增了一个单独的 \(d_i\),总共多了 \(i \cdot d_i\),所以

\[x' = x + i \cdot d_i \]

新平方和: 原来平方和是 \(\sum a_k^2\),现在新平方和 =

\[0^2 + d_i^2 + \sum_{k=2}^{i} (d_i + a_k)^2 \]

展开:

\[= d_i^2 + \sum_{k=2}^{i} (a_k^2 + 2d_i a_k + d_i^2) = \sum_{k=1}^{i} a_k^2 + 2d_i \sum_{k=2}^{i} a_k + i d_i^2 \]

\(\sum_{k=1}^{i} a_k = x\),所以增加量为:

\[2x d_i + i d_i^2 \]

因此新平方和:

\[f(i+1,\ x+i d_i) = \min(f(i+1,\ x+i d_i),\ f(i,x) + 2x d_i + i d_i^2) \]

这就是放左边的转移:

\[f(i,x) + 2x d_i + i d_i^2 \longrightarrow f(i+1,\ x + i d_i) \]

  • 放右边

当前数列仍然是 \(0, a_2, \dots, a_i\)

\(d_i\) 作为最后一个差分追加到末尾,新数列为:

\[0,\ a_2,\ \dots,\ a_i,\ a_i + d_i \]

原来的最后一个元素 \(a_i\) 等于前面所有差分之和,也就是 \(s_{i-1}\)
所以新增加的元素是:

\[a_i + d_i = s_{i-1} + d_i = s_i \]

新和: 增加了一个 \(s_i\),所以:

\[x' = x + s_i \]

新平方和: 只增加了一个 \(s_i^2\),所以:

\[f(i+1,\ x + s_i) = \min(f(i+1,\ x + s_i),\ f(i,x) + s_i^2) \]

这就是放右边的转移:

\[f(i,x) + s_i^2 \longrightarrow f(i+1,\ x + s_i) \]

此时时间复杂度 \(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 矩阵乘法:

\[C[i][j]=max_k\{A[i][k]+B[k][j]\} \]

类似普通矩阵乘法一样,这个式子具有结合律,所以我们可以直接重载乘法,然后用快速幂计算。

但是矩阵乘法只能从 \(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分

posted on 2026-07-16 20:51  郑浩崎  阅读(6)  评论(0)    收藏  举报