Luogu P2258 [NOIP 2014 普及组] 子矩阵 题解 [ 蓝 ] [ 枚举 ] [ 背包 DP ]
子矩阵:简单枚举加背包 DP,用到了枚举矩阵首行 / 首列的套路。
注意到 \(n,m\) 很小,考虑暴力,\(O(C_{n}^{r})\) 枚举出所选的行后,如果再继续枚举列,时间复杂度就是 \(O(C_{n}^{r}C_{m}^{c})\) 的,由于 \(n=16,r=8\) 时单个组合数有极值 \(12870\),故剪枝后可以轻松通过。
但本题还有一个性质:在确定子矩阵的首行后,对后续所选的列的决策是容易的。
于是定义 \(dp_{i,j}\) 表示确定了子矩阵的行后,目前选到第 \(i\) 列,前面一共选了 \(j\) 列的最小价值。把这个 DP 看做一个背包,每一列看做一个物品,枚举每一列前面接到的列转移即可。时间复杂度 \(O(C_{n}^{r}m^3)\)。
注意列之间的代价和某一列内的代价需要预处理出来才可以做到单次 DP \(O(m^3)\) 的复杂度。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
const int N=20;
int n,m,r,c,a[N][N],dp[N][N],w[N][N],ans=0x3f3f3f3f;
void check(int st)
{
int popc=0;
for(int i=0;i<n;i++)popc+=((st>>i)&1);
if(popc!=r)return;
memset(w,0,sizeof(w));
for(int i=1;i<=m;i++)
{
int lst=-1;
for(int j=1;j<=n;j++)
if((st>>(j-1))&1)
{
if(lst!=-1)
w[i][i]+=abs(a[j][i]-lst);
lst=a[j][i];
}
for(int j=i-1;j>=1;j--)
for(int k=1;k<=n;k++)
if((st>>(k-1))&1)
w[j][i]+=abs(a[k][i]-a[k][j]);
}
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=m;i++)dp[i][1]=w[i][i];
for(int lv=2;lv<=c;lv++)
for(int i=1;i<=m;i++)
for(int j=1;j<i;j++)
dp[i][lv]=min(dp[i][lv],dp[j][lv-1]+w[i][i]+w[j][i]);
for(int i=1;i<=m;i++)ans=min(ans,dp[i][c]);
}
int main()
{
//freopen("sample.in","r",stdin);
//freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>r>>c;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=0;i<(1<<n);i++)check(i);
cout<<ans;
return 0;
}

浙公网安备 33010602011771号