1. 程式人生 > 其它 >普通DP——逆序對構造(CF-Abnormal Permutation Pairs (easy version))

普通DP——逆序對構造(CF-Abnormal Permutation Pairs (easy version))

普通DP——逆序對構造(CF-Abnormal Permutation Pairs (easy version))

題目傳送門:Abnormal Permutation Pairs (easy version)

題目中說到有兩個序列p,q:

題目要求p的字典序要比q小,同時p的逆序對的數量要比q多。這個時候我們要將整個序列拆成兩份來看一個是前面相同的部分,另外一個是後面不同的部分(p和q第一個字母不同開始的位置到結尾)

為什麼要分成兩部分呢,首先為了保證p比q小,所以從前往後比大小,直到小的時候,這樣我們就可以對前一半和後一半進行分析。

我先順應大佬的思路來想:(先對後一半部分進行分析)

確定的情況下(),同時我們肯定在i之前的字元p和q序列是一樣的。這樣我們對後面一半進行分析因為這個位置的字母導致q這個序列比p這個序列的逆序對的數量多個(的值分別意思是在後一半段的序列中分別是排名大小是的,因為前一半段都一樣我們可以排除前一半段的影響因素)。

為了達到說明

這個時候說明我們在知道的時候可以直接通過控制的大小來算出到底有多少種。


那麼重點來了怎麼算出一個長度為 的序列的逆序對數為的時候到底有多少中排列呢?

首先我們這樣考慮當只有n個字元的時候,他們逆序對數量為m,我拿出一個比這個n個字元都大的字元g,假如我插入到第h個位置那麼原本n個字元被g字元分成了兩半,g前面有h-1個字元,g後面有n-h+1個字元,那麼由於g的插入創造出了n-h+1個逆序對。原本m個逆序對現在變成了m+n-h+1個逆序對。

順應這個思路很容易發現前面這個問題完全可以用來構造:

f[0][0] = 1;
for(int i = 0; i <= n*(n-1)/2; i++) s[0][i] = 1;
for(int i = 1; i <= n; i++)
{
	for(int j = 0; j <= n*(n-1)/2; j++)
	{
		if(j-i>= 0)
			f[i][j] = s[i-1][j]-s[i-1][j-i];
		else 
			f[i][j] = s[i-1][j];
		s[i][j] = (j?s[i][j-1]:0)+f[i][j]; 
	}	
}

  

首先我們需要兩個陣列來解決長度n的序列有0,1,2...n-1個逆序對可以形成多少種排列的問題:

  • 表示長度為i有j個逆序對的排列到底有多少種

  • 表示

首先注意初始化

  • 在長度為0的序列逆序對為0的排列有一種(就是空)

  • 然後由於來進行初始化得出

初始化後可以迴圈計算,第一層迴圈的意思是這個序列長度為i,第二個迴圈意思是這個長度為i的序列有j個逆序對,這樣的話假如說原本長度為i-1的序列由於第i個字母的加入可以創造出個逆序對。所以只能從轉移得到結果。

所以根據字首和可以寫出

f[i][j] = s[i-1][j]-s[i-1][j-i];

  

但是由於j-i會小於0所以它最多也只能從轉移過來了

然後下面這句話求的是長度為i的字串有個逆序對的

s[i][j] = (j?s[i][j-1]:0)+f[i][j];

  



#include<iostream>
#include<string>
using namespace std;
int n,mod,f[55][2005]={1},s[55][2005]={1},ans[55];

int main()
{
	cin >> n >> mod;
	f[0][0] = 1;
	for(int i = 0; i <= n*(n-1)/2; i++) s[0][i] = 1;
	for(int i = 1; i <= n; i++)
	{
		for(int j = 0; j <= n*(n-1)/2; j++)
		{
			if(j-i>= 0)
				f[i][j] = (s[i-1][j]-s[i-1][j-i]+mod)%mod;
			else 
				f[i][j] = s[i-1][j]%mod;
			s[i][j] = ((j?s[i][j-1]:0)+f[i][j])%mod; 
		}	
	}
	
	for(int i = 1; i <= n; i++)
	{
		for(int j = 1; j <= i; j++)
		{
			for(int z = j+1; z <= i; z++)
			{
				for(int g = 0; g <= i*(i-1)/2; g++)
				{
					int t = g-(z-j)-1;
					if(t < 0) continue;
					else (ans[i] += 1ll*f[i-1][g]*s[i-1][t]%mod)%=mod;
				}
			}
		}
	}
	
	for(int i = 2; i <= n; i++) ans[i] = (ans[i] + 1ll*ans[i-1]*i%mod)%mod;
	cout << ans[n];
}

  



現在我們回到這個原來的題目,先用第一層迴圈來遍歷後一半的長度,第二個迴圈和第三個迴圈列舉p和q後一半字串的第一個字元,最後一個迴圈代表p序列後一半的逆序對個數。這樣我們就可以滿足p比q小,但是直接算逆序對p比q小的情況。然後用(這個陣列代表後一半長度是i的情況下的符合題目要求的<p,q>的個數)來記錄。隨後

for(int i = 2; i <= n; i++) ans[i] = (ans[i] + 1ll*ans[i-1]*i%mod)%mod;

我們這句話的意思是遍歷了後面一半但是前面一半的字元組合還沒遍歷,這句話就是考慮到前面的組合了。至此整個題目完成。