题目链接:https://www.luogu.org/problemnew/show/P2933
题目大意就是给你一个集合,告诉你如何判定它的子集是否合法并让你找到一个最优子集
解法:
首先我们预处理出一个数组pre,pre[i][j]保存在i到j之间元素对误差的贡献,即我们枚举z(j-1>=z>=i+1),计算abs(2*m[z]-m[i]-m[j])
特殊的是,我们还需要处理出pre[i][0]和pre[i][n+1],分别表示在i之间和在i之后的元素对误差的贡献(感觉贡献这个词怪怪的)
预处理时间复杂度O(n3)
考虑如何DP
定义DP[i][j]表示前j个元素,必选第j个元素,总共选择了i个产生的最小误差。为什么把i放在前,j放在后呢?因为我们首先最小化的是子集的大小。状态转移方程就是:
dp[i][j]=min(dp[i][j],dp[i-1][q]+sum)(i-1<=q<=j)
我们有sum=-pre[q][n+1]+pre[q][j]+pre[j][n+1](之前我们是把q当成是子集的结尾并加上了它之后对误差的贡献,于是此时我们减去这个值改为用j来作为最后一个元素)
DP时间复杂度O(n3)
注意i=1的情况我们提前处理出来就是了
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<math.h> #define ri register int #define ll long long using namespace std; const int maxn=100+15; const int inf=0x3f3f3f3f; int n,e,k; ll ans; int m[maxn]; ll dp[maxn][maxn],pre[maxn][maxn]; int main() { scanf("%d%d",&n,&e); for (ri i=1;i<=n;i++) scanf("%d",&m[i]); for (ri i=1;i<=n;i++) { for (ri j=i+1;j<=n;j++) for (ri k=i+1;k<=j-1;k++) pre[i][j]+=abs(2*m[k]-m[i]-m[j]); for (int j=1;j<i;j++) pre[i][0]+=2*abs(m[j]-m[i]); for (int j=i+1;j<=n;j++) pre[i][n+1]+=2*abs(m[j]-m[i]); } k=n;ans=inf; for (ri i=1;i<=n;i++) { dp[1][i]=pre[i][0]+pre[i][n+1]; if (dp[1][i]<=e) { k=1; if (dp[1][i]<ans) ans=dp[1][i]; } } for (ri i=2;i<=n;i++) { for (ri j=i;j<=n;j++) { dp[i][j]=inf; for (ri q=i-1;q<j;q++) { int sum=-pre[q][n+1]+pre[q][j]+pre[j][n+1]; dp[i][j]=min(dp[i][j],dp[i-1][q]+sum); } if (dp[i][j]<=e) { if (i<k) {k=i;ans=dp[i][j];} if (i>k) continue; if (i==k) ans=min(ans,dp[i][j]); } } } printf("%d %lld ",k,ans); return 0; }