動態規劃(4):消除後效性

無後效性:

這是DP中最重要的一點, 他要求每個子問題的決策不能對後面其他未解決的問題產影響, 如果產生就無法保證決策的最優性, 這就是無後效性。往往需要我們找到一個合適的狀態。上述的問題還有另外一個描述方式, 對於後一個節點的判斷不能以前面節點的路徑爲依據。

例:POJ 1037 一個美妙的柵欄

N 個木棒, 長度分別爲1, 2, …, N.

構成美妙的柵欄要求

1.除了兩端的木棒外,每一跟木棒,要麼比它左右的兩根都長,要麼比它左右的兩根都短。
2.即木棒呈現波浪狀分佈,這一根比上一根長了,那下一根就比這一根短,或反過來

如下圖就是N等於4時的所有可能性

這裏寫圖片描述

問題:

符合上述條件的柵欄建法有很多種,對於滿足條件的所有柵欄, 按照字典序(從左到右, 從低到高) 排序。
給定一個柵欄的排序號,請輸出該柵欄, 即每一個木棒的長度.

問題抽象:

問題抽象:給定1到N 這N個數字,將這些數字高低交替進行排列 ,把所有符合情況的進行一個字典序排列,問第C個排列是一個怎樣的排列

試着解題

按照上文的DP常用方法我們依次做如下分析:
·
·

直接將問題的範圍縮小

設 A[i] 爲i根木棒所組成的合法方案數目。看看能否找出A[i]和A[i-1]或A[i-j]之間的遞推關係(所有木棒總數是i)。稱i根木棒的合法方案集合爲S(i)。

對於這種方法, 我們可以想到, 在i種木棍的情況下, 假設我們有N中方案數。 能否在這N種方案的最後插入第i+1根木棒, 或者如何在這I根中插入第i+1根,也跟前面的選擇方案有關, 所以在這裏, 這種簡單粗暴的方式並不具備後效性(但不得不說 在水DP中這依舊是對絕大多數題目有效的方法)
·
·

加限制條件

在選定了某根木棒x作爲第一根木棒的情況下,剩下i-1根木棒的合法方案數是A[i-1]。但是,這A[i-1]種方案,並不是每種都能和x形成新的合法方案。將第一根比第二根長的方案稱爲DOWN方案,第一根比第二根短的稱爲UP方案,則,S(i-1)中,第一根木棒比x長的DOWN方案,以及第一根木棒比x短的UP方案,才能和x構成S(i)中的方案。

這裏我們對木棒的條件進行條件限制, 在加入第i根木棒的情況下我們記錄當前情況的上升和下降的情況。
·
·

繼續推導

置A[i] = 0。先枚舉x。然後針對每個x,枚舉x後面的那根木棒y。如果 y > x(x < y的情況類推),則:

A[i] += 以y打頭的DOWN方案數

但以y打頭的DOWN方案數,又和y的長短有關。

於是難以直接從 A[i-1]或 A[i-j]推出 A[i]

也就是這裏依舊不具備後效性
·
·
·

繼續加限制條件

考慮將A[i]這種粗略的狀態描述方式細化,即加上限制條件後分類。設

A[i] = Σ B[i][k] k = 1….i

B[i][k] 是S(i)中以第k短的木棒打頭的方案數。

嘗試對 B 進行動歸。第k短,指的是i根木棒中第k短。
·
·
·

綜合上述的全部限制條件

B[i][k] = Σ B[i-1][M](D OWN) + Σ B[i-1][N](U P)

M = k … i-1 , N = 1… k-1

還是沒法直接推。於是把B再分類細化:

B[i][k] = C[i][k][D OWN] + C[i][k][U P]

C[i][k][DOWN] 是S(i)中以第k短的木棒打頭的DOWN方案數。然後試圖對C進行動歸

C[i][k][UP] = Σ C[i-1][M][DOWN]
M = k … i -1
C[i][k][DOWN] = Σ C[i-1][N][UP]
N = 1… k-1
初始條件:C[1][1][UP]=C[1][1][DOWN] = 1

思路總結

當選取的狀態,難以進行遞推時(分解出的子問題和原問題形式不一樣,或不具有無後效性),考慮將狀態增加限制條件後分類細化即增加維度,然後在新的狀態上嘗試遞推

另外這題目的另一個難點就是關於排列計數的問題, 這個問題在這裏暫時不講

代碼

#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 25

using namespace std;

long long dp[maxn][maxn][2];

void init(int n)
{
    memset(dp, 0, sizeof(dp));
    dp[1][1][0] = 1;//up
    dp[1][1][1] = 1;//down
    for (int j = 2; j <= n; j++)
    {
        for (int i = 1; i <= j; i++)
        {
            for (int x = i; x < j; x++)
                dp[i][j][0] += dp[x][j - 1][1];
            for (int x = 1; x < i; x++)
                dp[i][j][1] += dp[x][j - 1][0];
        }
    }
}

int ans[maxn];

void solve(int n, long long c)
{//下面主要是排列組合的相關問題
    int tn = n;
    int flag, cur;
    long long sum = 0LL;
    for (int i = 1; i <= n; i++)
    {
        if (sum + dp[i][n][0] + dp[i][n][1] >= c)
        {
            ans[1] = i;
            c -= sum;
            cur = i;
            break;
        }
        sum += (dp[i][n][0] + dp[i][n][1]);
    }
    if (c < dp[cur][n][1])
        flag = 1;
    else
    {
        c -= dp[cur][n][1];
        flag = 0;
    }
    --n;
    int len = 2;
    while (n > 0)
    {
        if (flag == 0)
        {
            for (int i = cur; i <= n; i++)
            {
                if (dp[i][n][1] >= c)
                {
                    cur = i;
                    ans[len++] = cur;
                    break;
                }
                c -= dp[i][n][1];
            }
        }
        else
        {
            for (int i = 1; i<cur; i++)
            {
                if (dp[i][n][0] >= c)
                {
                    cur = i;
                    ans[len++] = cur;
                    break;
                }
                c -= dp[i][n][0];
            }
        }
        --n;
        flag = 1 - flag;
    }
    int vis[maxn] = { 0 };
    for (int i = 1; i <= tn; i++)
    {
        for (int j = 1; j <= tn; j++)
        {
            if (vis[j] == 0)
            {
                ans[i]--;
                if (ans[i] == 0)
                {
                    printf("%d%c", j, i == tn ? '\n' : ' ');
                    vis[j] = 1;
                    break;
                }
            }
        }
    }
}

int main(void)
{
    int T, n;
    long long c;
    scanf("%d", &T);
    while (T--)
    {
        scanf("%d %lld", &n, &c);
        solve(n, c);
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章