子矩阵 解题报告
题目大意
在一个N行M列的矩阵中,选出一个R行C列的子矩阵,使相邻元素的差的绝对值的和最小。
解题方法
1. 暴力出奇迹
解题思路
在一个N行M列的矩阵中,选出一个 \(R\) 行 \(C\) 列的子矩阵,相当于一个 \(N\) 行 \(M\) 列的矩阵,在 \(N\) 行中选出 \(R\) 行,在 \(M\) 列中选出 \(C\) 列,选出的行和列相交的点就是选出的数。
所以一个很简单的办法,就是枚举 \(N\) 行中选哪 \(R\) 行, \(M\) 列中选哪 \(C\) 列。这就是一个组合问题,相当于枚举 \(R \choose N\) 和 \(C \choose M\)。
这个算法的时间复杂度,就是枚举的 \(R \choose M\) \(\times\) \(C \choose M\) 在乘上计算的 \(R \times C\) ,刚好可以得60分。
代码大致是这样实现的:
- 枚举 \(R \choose M\)
- 枚举 \(R \choose M\)
- 在第二步枚举完后求值,更新答案
枚举组合,可以用DFS来实现:
int n, r, a [ MaxN + 1 ];
void solve ( int step )
{
if ( step > r )
{
print ( );
return ;
}
for ( int i = a [ step - 1 ] + 1; i <= n; i ++ ) a [ step ] = i, solve ( step + 1 );
}
你可以拿这个代码去实现一些题目(当然不行),你会发现,在 \(N<=15\) 的时候大致可以过(主要看评测机是赛扬还是i99900k),但是你可以试一试这道题,TLE的很厉害。
仔细观察i的范围,这里显然可以做一些优化。
"\(i <= n\)" 这个范围很不合理,如果剩余空间不够 \(r - step\) 个,肯定不可能。
因此,\(n - i\) 必须 \(>= r - step\),转化得到 \(i <= n - r + step\)。
代码只用修改一处,就是把 for ( int i = a [ step - 1 ] + 1; i <= n; i ++ )
变为 for ( int i = a [ step - 1 ] + 1; i <= n - r + step; i ++ )
。
代码1
回到这道题目上,代码实现很简单:
//主程序请自行实现
void dfs1 ( int step )
{
if ( step > r ) { dfs2 ( 1 ); return ; }
for ( int i = row [ step - 1 ] + 1; i <= n - r + step; i ++ ) row [ step ] = i,dfs1 ( step + 1 );
}
void dfs2 ( int step )
{
if ( step > c )
{
int sum = 0;
//统计分数部分请自行实现
if ( sum < ans ) ans = sum;
return ;
}
for ( int i = col [ step - 1 ] + 1; i <= m - c + step; i ++ ) col [ step ] = i,dfs2 ( step + 1 );
}
2. 动态规划
如果用DP,可以 参考其他题目 这样写:
\(f [ n ] [ r ] [ m ] [ c ]\) 表示 \(n\) 列中取 \(r\) 列,\(m\) 列中取 \(c\) 列,第 \(n\) 行和第 \(m\) 列必须取时的最小分值。
但这样写会出现状态不明确的问题,也就是有后效性。
不能全部用DP,那咋办???
别急,看后面 》》》
3. 一半DFS一半DP
解题思路
既然全部DP或者记忆化状态会不明确,那么通过再枚举就可以确定状态了(这里选择列)。
\(f [ i ] [ j ]\) 表示 \(i\) 列中取 \(j\) 列,第 \(i\) 列必需取的最小分值
\(f [ i ] [ j ] = \min_{ f [ k ] [ j - 1 ] + \sum_{ x = 1 } ^ { r - 1 } { | a [ row [ x ] ] [ i ] - a [ row [ x - 1 ] ] [ i ] | } + \sum_{ x = 1 } ^ { r } { | abs ( a [ row [ x ] ] [ i ] - a [ row [ x ] ] [ j ] | } }\)
(\(2 \leq i \leq m, 2 \leq j \leq min ( i, c ), j - 1 \leq k < i\))
这样写出的DP大致是这么一坨东西:
/*
请先预处理:
col [ i ] 表示i列内部的分值(相当于第一个sigma)
cc [ i ] [ j ] 表示第i列与第j列相邻时,他们列之间相差的分值(相当于第二个sigma)
*/
for ( int i = 2; i <= m; i ++ )
for ( int j = 2; j <= min ( i, c ); j ++ )
for ( int k = j - 1; k < i; k ++ )
f [ i ] [ j ] = min ( f [ i ] [ j ], f [ k ] [ j - 1 ] + col [ i ] + cc [ k ] [ i ] );
再结合一下之前的DFS,写出代码:
代码2
//同样,主程序请自行实现
//码风小清新,多多照顾
//适当做了一些优化(630ms)
void dfs ( int x )
{
if ( x > r )
{
for ( int i = 1; i <= m; i ++ )
{
for ( int j = 1; j <= m; j ++ )
{
cc [ i ] [ j ] = 0;
for ( int k = 1; k <= r; k ++ )
{
cc [ i ] [ j ] += abs ( a [ row [ k ] ] [ i ] - a [ row [ k ] ] [ j ] );
}
}
}
for ( int i = 1; i <= m; i ++ )
{
col [ i ] = 0;
for ( int j = 1; j < r; j ++ )
{
col [ i ] += abs ( a [ row [ j ] ] [ i ] - a [ row [ j + 1 ] ] [ i ] );
}
}
for ( int i = 1; i <= m; i ++ )
{
f [ i ] [ 1 ] = col [ i ];
}
for ( int i = 2; i <= m; i ++ )
{
for ( int j = 2; j <= min ( i, c ); j ++ )
{
f [ i ] [ j ] = 0x3f3f3f3f
for ( int k = j - 1; k < i; k ++ )
{
f [ i ] [ j ] = min ( f [ i ] [ j ], f [ k ] [ j - 1 ] + col [ i ] + cc [ k ] [ i ] );
}
}
}
for ( int i = c; i <= m; i ++ )
{
res = min ( f [ i ] [ c ], res );
}
return;
}
for ( int i = row [ x - 1 ] + 1; i <= n - r + x; i ++ )
{
row [ x ] = i;
dfs ( x + 1 );
}
}
总结时间到!
诶,这道题主要是一个DFS和DP结合的玩意。
当发现DFS转DP会产生后效性时,有两种方法:
- 加状态
- 再搜索
第一种方法显然不现实 十六维数组,为啥不给把16G内存全用上呢?(手滑=手动滑稽
那就只能用第二种方法,就产生了这一坨DFS+DP的东东。
解这种烦人的村民题时,一般先打一个暴力(NOIP2014,如果前三题AC,一等奖稳稳的),再去优化成DP OR 记忆化。
哎吗,这篇题解写了我好几年哟,赞一个呗