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

时间:2021-07-12
本文章向大家介绍普通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;

我们这句话的意思是遍历了后面一半但是前面一半的字符组合还没遍历,这句话就是考虑到前面的组合了。至此整个题目完成。

原文地址:https://www.cnblogs.com/hyb-bbfss/p/15003388.html