题解/算法 {3149. 找出分数最低的排列}
@LINK: https://leetcode.cn/problems/find-the-minimum-cost-array-permutation/
;
说明你要明白 这个公式 讲的是个啥意思。。。
A[N]
, 比如你选择的排列是[a,b,c]
, 那么答案是: abs( A[b]-a]) + abs(A[c]-b) + abs(A[a]-c)
; 换句话说, 我们令V[a] = abs( A[b]-a); V[c] = abs(A[a] - c)
, 即答案是V[a] + V[b] + V[c]
;
很容易想到一个DP( i, j, beg, ed)
对于排列[0,1,...,i]
其中放入了j(二进制1的下标, j的1个数==[i+1])
这些数 [0]==beg, [i]==ed
, 的最小的V[0]+V[1] + ...+V[i]
之和;
他可以求出来 DP的值, 可是, 由于他是正向DP 他是无法求出最小字典序的DP路径的; 比如 当前DP的前驱DP有[ PRE1, 3] 和 [ PRE2, 4]
你选择哪个? PRE1, PRE2
他俩 都有可能是最小字典序 因此你无法通过其末尾元素3,4
来判断 哪个是最小字典序;
这是第一个问题; 其次, 我们看你的DP定义, 这个beg
完全没必要放到DP状态里面 你就直接枚举beg
的值 然后用DP求形如[beg, ...]
的排列即可, DP状态越少 代码思路越明确;
再其次, 其实你的i
状态 是可以去掉的, 这是状压DP优化排列序列的一个技巧 因为现在你是i(N) * j(1<<N)
但此时你会枚举很多非法状态 对于i
来说 其实只有二进制里有(i+1)个1
的状态j
是合法的 但你会枚举1<<N
个状态, 那么要怎么优化呢? 令 i
的所有合法状态j
的集合是ST(i)
则他会满足ST(0) < ST(1) < ...
这里的小于是指 ST(i)
里面的任一状态j
都是严格小于 ST(>i)
里面的状态, 因为二进制1
的个数 是严格递增的; 因此(i,j)
可以直接优化为 (j)
即枚举[0,1,2,..., (1<<N))
;
关键是第一个问题, 即 怎么求最小字典序呢?
用反向DP; 对于正向DP 即对于[PRE, ed] cur
仅仅根据ed
你是无法保证这个序列是最小字典序; 但对于反向DP 对于cur [beg, SUF]
此时仅仅根据beg
你是可以保证这个序列是最小字典序的(只要选择最小的beg
即可) 因为cur, [1, [2,4的排列]]
字典序一定小于cur, [2, [1,4的任意排列]]
; (即对于一个排列来说(即所有元素都不同) 你只要保证最小元在开头 那么他的字典序 一定小于 任意的(最小元不在开头)的排列;
即对于[0,1,....,N]
, 反向DP是倒着来的 即先设置[N]
, 然后更新到[N-1,N]
然后更新到[i,...,N]
;
DP定义是: 枚举Beg
(即[0]
位置上的元素), 然后DP是根据beg进行的 DP(I, J, K)
将J所表示的元素 放到[I, I+1,...,N]
这些位置上 且[I]
上的元素是K
(还有个大前提 即A[0]==beg
即对于beg
你要特判 不能放到其他位置上); (我们上面讲过 这里的(I,J)
其实可以优化成(J)
因为I == J里1的个数
);
.
这个DP 用前驱/后继更新方式呢? 其实都可以, 只要能更新DP即可; 注意 不管是哪种更新方式 DP路径都是记录的前驱, 因为我们遍历DP路径是 从DAG的叶子节点(因为答案在这里) 然后回溯去找其前驱节点, 一定是这样的, 因此 如果你DP是后继更新 cur-> nex
那么你记录的是DPrec[ nex] = cur
(记录前驱 即是谁更新的我), 如果DP是前驱更新pre -> cur
那么DPrec[cur] = pre
, 本质上两者是一样的;
.
但是, 他会影响字典序; 比如说用后继更新 得到了[i,...N],ed
去更新i-1 [i,...,N]
, 然后你[i,...N], ed2
也会更新i-1 [i...N]
同一个DP状态, 假如他俩的DP值一样 那么这个DPrec[nexDP]
要记录谁呢? 要记录min( ed, ed2)
他俩的最小值, 这就有麻烦 当然也可以做, 你对ed
的遍历 要从小到大 这样会优先选择ed
小的; 用前驱方式 就方便的多 对于[i....N]
要他的前驱[i+1, ...N]
然后让[i+1]
最小即可 (反向DP 一般使用DFS方式比较方便);
其实… 仔细观察你会发现, 他计算的 其实是循环数组, 也就是[a,b,c]的值 == [c,a,b]的值 == [b,c,a]的值
; 因此, 其实起点是固定的 beg==0
即你只需要枚举 形如[0, ...]
的排列;
反向DP (DFS)
反向DP 一般都用DFS方式, 他是前驱更新方式 (即当前DP值不知道 先得到前驱节点(DFS回溯) 来更新当前节点);
vector<int> findPermutation(vector<int>& A) {
int N = A.size();
static int DP[ 1<<14][ 14];
static int DPrec[ 1<<14][ 14];
constexpr int VALID = 0x7F7F7F7F;
int dpV = VALID;
vector<int> ANS(N);
FOR_( beg, 0, 0){ // 不用写成`(0, N-1)`;
FOR_( i, 0, (1<<N)-1){ FOR_( j, 0, N-1){ DP[i][j] = VALID;}}
auto Dfs = [&, beg]( auto _dfs, int _mask, int _end)->int{
auto & curDP = DP[ _mask][ _end];
auto & curDPrec = DPrec[ _mask][ _end];
if( curDP != VALID){ return curDP;}
if( ___Binary_BitsCount(_mask) == 1){
curDP = std::abs( A[beg] - _end);
}
else{
FOR_( i, 0, N-1){ // 这里的顺序 决定了字典序
if( ((_mask>>i) & 1) == 0){ continue;}
if( i == _end){ continue;}
auto V = _dfs( _dfs, _mask ^ (1<<_end), i) + std::abs( A[i] - _end);
if( V < curDP){
curDP = V;
curDPrec = i; // 记录前驱节点;
}
}
}
return curDP;
}; // not iterating using `ind`, we iterate on the BisCount of `mask`; (Note, dfs-dp is not focus on `Array's index`, it focus on the Parameters-Of-Dfs;
auto res = Dfs( Dfs, (1<<N)-1, beg);
if( res < dpV){
dpV = res;
int mask = (1<<N)-1, ed = beg;
FOR_( i, 0, N-1){
ANS[ i] = ed;
auto nex = DPrec[ mask][ ed];
mask ^= (1<< ed);
ed = nex;
}
}
}
return ANS;
}
反向DP (递推, 前驱方式)
vector<int> findPermutation(vector<int>& A) {
int N = A.size();
static int DP[ 1<<14][ 14];
static int DPrec[ 1<<14][ 14];
constexpr int VALID = 0x7F7F7F7F;
int dpV = VALID;
vector<int> ANS(N);
FOR_( Beg, 0, 0){ // 不用写成`(0, N-1)`;
FOR_( i, 0, (1<<N)-1){ FOR_( j, 0, N-1){ DP[i][j] = VALID;}}
{
FOR_( _mask, 0, (1<<N)-1){
FOR_( _ed, 0, N-1){ // 这里的顺序 不重要;
if( ((_mask>> _ed) & 1) == 0){ continue;}
if( _ed==Beg && (_mask!=((1<<N)-1))){ continue;}
if( _mask==((1<<N)-1) && _ed!=Beg){ continue;}
auto & curDP = DP[ _mask][ _ed];
auto & curDPrec = DPrec[ _mask][ _ed];
if( _mask == (1<<_ed)){ curDP = std::abs( A[0] - _ed);}
else{
FOR_( JJ, 0, N-1){ // 这里的顺序 决定了字典序;
if( JJ==_ed || ((_mask>>JJ)&1)==0){ continue;}
auto dpV = DP[ _mask ^ (1<<_ed)][ JJ] + std::abs( A[JJ] - _ed);
if( dpV < curDP){
curDP = dpV;
curDPrec = JJ;
}
}
}
}
}
}
auto res = DP[ (1<<N)-1][ Beg];
if( res < dpV){
dpV = res;
int mask = (1<<N)-1, ed = Beg;
FOR_( i, 0, N-1){
ANS[ i] = ed;
auto nex = DPrec[ mask][ ed];
mask ^= (1<< ed);
ed = nex;
}
}
}
反向DP (递推, 后继方式)
vector<int> findPermutation(vector<int>& A) {
int N = A.size();
static int DP[ 1<<14][ 14];
static int DPrec[ 1<<14][ 14];
constexpr int VALID = 0x7F7F7F7F;
int dpV = VALID;
vector<int> ANS(N);
FOR_( beg, 0, 0){ // 不用写成`(0, N-1)`;
FOR_( i, 0, (1<<N)-1){ FOR_( j, 0, N-1){ DP[i][j] = VALID;}}
{
FOR_( i, 0, N-1){
if( i == beg){ continue;}
DP[ 1<<i][ i] = std::abs( A[beg] - i);
}
FOR_( _mask, 0, (1<<N)-1){
FOR_( _ed, 0, N-1){ // 必须从小到大 才能保证字典序;
if( DP[_mask][ _ed] == VALID){ continue;}
FOR_( i, 0, N-1){
if( (_mask>> i) & 1){ continue;}
if( i==beg && (___Binary_BitsCount(_mask)!=N-1)){ continue;}
auto newV = DP[_mask][_ed] + std::abs( A[_ed] - i);
auto & nexDP = DP[ _mask | (1<< i)][ i];
if( newV < nexDP){
nexDP = newV;
// DPrec[ _mask][ _ed] = i; 这就错了! DP路径记录的 必须是*当前节点的前驱*(即谁更新的我);
DPrec[ _mask | (1<< i)][ i] = _ed;
}
}
}
}
}
auto res = DP[ (1<<N)-1][ beg];
if( res < dpV){
dpV = res;
int mask = (1<<N)-1, ed = beg;
FOR_( i, 0, N-1){
ANS[ i] = ed;
auto nex = DPrec[ mask][ ed];
mask ^= (1<< ed);
ed = nex;
}
}
}
return ANS;
}
笔记
用递推的话, 你能求出DP值 但是你无法求路径, 比如[..., 4] 和 [..., 5]
你选哪个路径? 他俩都可能是答案;
求最小字典序, 你要从前往后贪心, 而你的DP是从后往前的,所以是错误的;
因此: 要用DFS式DP(即反向DP),这样你是从前往后的, 既可以求答案 也可以求路径;