枚举组合数更少一维的全部下标组合,把分值拆成纵向 v 与横向 h 两项,在另一维上做选 c 个的顺序 DP 求最小分值。
OJ: roj
题目 ID: 3617
难度:普及+/提高-
标签:动态规划线性DP枚举python
创建: 2026-10-02 10:42
更新: 2026-10-10 11:30
形式化题目
给定一个
记选中的行集为
样例 1 中选中第 4、5 行与第 1、3、4 列,交叉格加粗如下:
| 行 |
1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 1 | 9 | 3 | 3 | 3 | 9 |
| 2 | 9 | 4 | 8 | 7 | 4 |
| 3 | 1 | 7 | 4 | 6 | 6 |
| 4 | 6 | 8 | 5 | 6 | 9 |
| 5 | 7 | 4 | 5 | 6 | 1 |
这个
正解
思路
朴素做法:枚举所有行组合与列组合,按定义逐对累加,共
关键观察:固定行集
- 纵向部分对每个列
独立求和,记 , 只依赖 和列 ,与选哪些列无关; - 横向部分只有当列
、 在子矩阵中相邻时才计入,记 , 只依赖 和列对 。
于是固定
"选
; ,表示已选 个列、最右一个是 的最小代价; 保证列下标递增(即保持列的 相对顺序),末位取不到更小位置时该状态为 ;
遍历顺序先
样例 DP 表
下面固定行组合
| 已选列数 |
1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 1 | 1 | 4 | 0 | 0 | 8 |
| 2 | 10 | 4 | 2 | 16 | |
| 3 | 14 | 6 | 18 |
观察重点:第
它的前驱是
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = (ll)1000000000000000000LL;
ll comb_count(int n,int r){ if(r<0||r>n) return 0; if(r>n-r) r=n-r; ll ans=1; for(int i=1;i<=r;i++) ans=ans*(n-r+i)/i; return ans; }
void gen_combos(int n,int take,int start,vector<int>&cur,vector<vector<int> >&all){ if((int)cur.size()==take){all.push_back(cur);return;} for(int i=start;i<n;i++){cur.push_back(i); gen_combos(n,take,i+1,cur,all); cur.pop_back();}}
ll best_score(vector<vector<ll> > &mat,int take,int pick){ int height=mat.size(), width=mat[0].size(); vector<vector<ll> > flat(height, vector<ll>(width*width)); for(int r=0;r<height;r++) for(int i=0;i<width;i++) for(int j=0;j<width;j++) flat[r][i*width+j]=llabs(mat[r][i]-mat[r][j]); vector<vector<int> > all; vector<int> cur; gen_combos(height,take,0,cur,all); ll best=INF; for(int ci=0;ci<(int)all.size();ci++){vector<int> combo=all[ci]; vector<ll> v(width); for(int b=0;b<width;b++) for(int z=0;z+1<(int)combo.size();z++) v[b]+=llabs(mat[combo[z]][b]-mat[combo[z+1]][b]); vector<ll> h(width*width); for(int z=0;z<(int)combo.size();z++) for(int p=0;p<width*width;p++) h[p]+=flat[combo[z]][p]; vector<ll> dp=v; for(int step=1;step<pick;step++){ vector<ll> ndp(width,INF); for(int j=1;j<width;j++){ll mn=INF; int base=j*width; for(int i=0;i<j;i++) mn=min(mn, dp[i]+h[base+i]); ndp[j]=v[j]+mn;} dp=ndp;} for(int i=0;i<width;i++) best=min(best,dp[i]); } return best; }
int main(){ios::sync_with_stdio(false); cin.tie(nullptr); int n,m,r,c; if(!(cin>>n>>m>>r>>c)) return 0; vector<vector<ll> > rows(n, vector<ll>(m)); for(int i=0;i<n;i++) for(int j=0;j<m;j++) cin>>rows[i][j]; if(comb_count(n,r)<=comb_count(m,c)) cout<<best_score(rows,r,c)<<'\n'; else {vector<vector<ll> > tr(m, vector<ll>(n)); for(int i=0;i<n;i++) for(int j=0;j<m;j++) tr[j][i]=rows[i][j]; cout<<best_score(tr,c,r)<<'\n';} return 0;}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 10:44
# update_at: 2026-10-02 10:44
import sys
from itertools import combinations
from math import comb
INF = 10**18 # 分值远小于它,只用来标记"还没选够列"的无效状态
def best_score(mat: list[list[int]], take: int, pick: int) -> int:
"""枚举第一维的 take 个下标组合,在第二维上选出 pick 个下标,返回最小分值。
固定第一维组合后,分值恰好拆成两部分:第二维每个位置上的纵向相邻差之和 v,
以及第二维任意两位置之间横跨所选行的横向差之和 h;第二维上做一次
"选 pick 个、末位是 j" 的顺序 DP 即可把两部分拼成完整分值。
"""
width = len(mat[0])
# 每行压平成 width×width 的 |差| 方阵:加入某行时用 zip 一次性加到 h 上
flat = [[abs(x - y) for x in row for y in row] for row in mat] # len(mat) 个方阵
best = INF
for combo in combinations(range(len(mat)), take):
# 纵向:该位置上相邻所选行之间的差之和(子矩阵内这些边必然存在)
v = [
sum(abs(mat[p][b] - mat[q][b]) for p, q in zip(combo, combo[1:]))
for b in range(width)
]
# 横向:第二维 i、j 相邻时,横跨所有所选行的差之和
h = [0] * (width * width)
for p in combo:
h = [x + y for x, y in zip(h, flat[p])]
dp = v[:] # 只选 1 个位置:只有纵向代价
for _ in range(1, pick):
cur = [INF] * width
for j in range(1, width): # 末位是 j,前一位只能取更小的 i
base = j * width
cur[j] = v[j] + min(dp[i] + h[base + i] for i in range(j))
dp = cur
best = min(best, min(dp))
return best
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
m = next(data)
r = next(data)
c = next(data)
rows = [[next(data) for _ in range(m)] for _ in range(n)]
# 枚举组合数更少的一维,另一维留给 DP,两侧取 min(C(n,r), C(m,c))
if comb(n, r) <= comb(m, c):
mat, take, pick = rows, r, c
else:
mat, take, pick = list(map(list, zip(*rows))), c, r
print(best_score(mat, take, pick))
if __name__ == "__main__":
solve()复杂度
设
即枚举行时是
空间复杂度 v/h/dp 各占
总结
本题的钥匙是把分值按"纵向 / 横向"拆开:固定一维的下标组合后,另一维上
每个位置的纵向代价