题解/算法 {3149. 找出分数最低的排列}

题解/算法 {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),这样你是从前往后的, 既可以求答案 也可以求路径;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值