無後效性:
這是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;
}