线性dp-计数类题目10(ZBRKA)

题目链接

解题过程

拿到这题时,我留意到说的是一个长度为 n n n的排列。一开始我也不知道怎么下手,于是就从最简单的开始列举。

  • n = 1 , c = 0 n=1,c=0 n=1,c=0,1种。
  • n = 1 , c = 1 n=1,c=1 n=1,c=1, 0种。
  • n = 2 , c = 0 n=2,c=0 n=2,c=0,1种(12)。
  • n = 2 , c = 1 n=2,c=1 n=2,c=1,1种(21)。
  • n = 2 , c = 2 n=2,c=2 n=2,c=2,0种。
  • n = 3 , c = 0 n=3,c=0 n=3,c=0,1种(123)。
  • n = 3 , c = 1 n=3,c=1 n=3,c=1,2种(132,213)。
  • n = 3 , c = 2 n=3,c=2 n=3,c=2,2种(312,231)。
  • n = 3 , c = 3 n=3,c=3 n=3,c=3,1种(321)。
  • n = 4 , c = 0 n=4,c=0 n=4,c=0,1种(1234)。
  • n = 4 , c = 1 n=4,c=1 n=4,c=1,3种(1243,1324,2134)
  • n = 4 , c = 2 n=4,c=2 n=4,c=2,5种(1423,1342,2143,3124,2314)
  • n = 4 , c = 3 n=4,c=3 n=4,c=3,6种(4123,1432,2413,3142,2341,3214)
  • n = 4 , c = 4 n=4,c=4 n=4,c=4,5种(4132,4213,3412,2431,3241)
  • n = 4 , c = 5 n=4,c=5 n=4,c=5,3种(4312,4231,3421)
  • n = 4 , c = 6 n=4,c=6 n=4,c=6,1种(4321)
    ...

通过列举,可以发现长度 i ( i > = 3 ) i(i>=3) i(i>=3)的排列,逆序对最大有 ( i − 1 ) ∗ i / 2 (i-1)*i/2 (i−1)∗i/2个。我们记 f i j fij fij为长度为 i i i的排列,逆序对为 j j j的排列总数,你会发现可以从长度为 i − 1 i-1 i−1的排列有 j j j个逆序对的情况转移过来(将 i i i插入到末尾);也可以从长度为 i − 1 i-1 i−1的排列有 j − 1 j-1 j−1个逆序对的情况转移过来(将 i i i插入到倒数第二的位置,这样就增加1个逆序对);还可以从长度为 i − 1 i-1 i−1的排列有 j − 2 j-2 j−2个逆序对的情况转移过来(将 i i i插入到倒数第三的位置,这样就增加2个逆序对)...这是思考一下,转移的边界是什么呢?

答案是要求第二维下标不越界,并且我们把 i i i插进长度为 i − 1 i-1 i−1的序列时有 i i i个位置,所以下边界 l = m a x ( 0 , j − i + 1 ) l=max(0,j-i+1) l=max(0,j−i+1),我们累加 f i − 1 l i − 1 j fi-1l到~i-1j fi−1li−1j的和。

基于上面分析,我们可以初始化 f 1 0 = 1 , f 1 1 = 0 f10=1,f11=0 f10=1,f11=0,之后用三重循环实现:

  • 最外层循环,循环变量 i i i从2开始枚举到 n n n,表示枚举长度为2到 n n n的排列
  • 中间层循环,循环变量 j j j从0枚举到 m i n ( c , i ∗ ( i − 1 ) / 2 ) ) min(c,i*(i-1)/2)) min(c,i∗(i−1)/2)),表示枚举长度为 i i i的排列,有 j j j个逆序对的方案数
  • 最内层循环,循环变量 k k k从 m a x ( 0 , j − i ) max(0,j-i) max(0,j−i)枚举到 j j j,长度为 i i i逆序对有 j j j对的方案数从 f i − 1 k fi-1k fi−1k转移过来,把它们累加起来。

考虑到数据规模n达到103,c达到104,这样的三重循环会超时。我们可以把内层循环用前缀和存储,优化时间复杂度。另外,我们还要对结果取模。

AC代码

第一版代码

一开始我写的很复杂,还差点越界了。这是第一版,用了滚动数组存储长度为 i − 1 i-1 i−1和长度为 i i i的排列,每次做最外层循环都要对 f 、 g 、 s f、g、s f、g、s数组更新,也很容易出错和越界。

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 1e9 + 7, maxc = 10000;
int f[10005], g[10005], s[10005];

signed main() {
	int n, c;
	cin >> n >> c;
	if (n == 1 && c == 1)
		cout << 0;
	else {
		f[0] = 1, f[1] = 1;
		s[0] = 1, s[1] = 2;
		for (int i = 3; i <= n; i++) {
			int cd = min(i * (i - 1) / 2, maxc);
			g[0] = 1;
			for (int j = 1; j <= cd; j++) {
				int l = max(0ll, j - i + 1), r = min(j, (i - 1) * (i - 2) / 2);
				if (l == 0)
					g[j] = (g[j] + s[r]) % mod;
				else
					g[j] = (g[j] + s[r] - s[l - 1] + mod) % mod;
			}
			for (int j = 1; j <= cd; j++)
				f[j] = g[j], g[j] = 0, s[j] = 0;
			for (int j = 1; j <= cd; j++)
				s[j] = (s[j - 1] + f[j]) % mod;
		}
		cout << f[c];
	}
	return 0;
}
第二版代码

之后借鉴题解,想到一些优化策略:

  1. 前缀和数组s每次都要清零,实际上我们可以令 s 0 = f 0 s0=f0 s0=f0,之后用 s j = s j − 1 + f j ( j > = 1 ) sj=sj-1+fj(j>=1) sj=sj−1+fj(j>=1),就可以把上一轮算好的 f f f加进去了。
  2. g g g数组和 f f f数组的优化:我们用g数组算完长度为 i i i的排列后 ,就把g的元素存到f数组中,准备长度为 i + 1 i+1 i+1的情况。在新的一轮循环中(第 i i i次),我们直到 f f f数组最多到 ( i − 2 ) ∗ ( i − 1 ) / 2 (i-2)*(i-1)/2 (i−2)∗(i−1)/2(因为长度为 i − 1 i-1 i−1时,最多有 ( i − 1 ) ∗ ( i − 2 ) / 2 (i-1)*(i-2)/2 (i−1)∗(i−2)/2个逆序对),这样很绕也很复杂。于是,我们想一下就看长度为 i i i的排列,最多有 i ∗ ( i − 1 ) / 2 i*(i-1)/2 i∗(i−1)/2个逆序对,所以我们内部的循环 j j j从0到 m i n ( c , i ∗ ( i − 1 ) / 2 ) min(c, i * (i - 1) / 2) min(c,i∗(i−1)/2)。加上我们之前探讨过数组下标不能越界, j j j累加的边界是 m a x ( 0 , j − i + 1 ) max(0,j-i+1) max(0,j−i+1)到 j j j,所以如果 j − i > = 0 j-i>=0 j−i>=0,那么 f j = ( s j − s j − i + m o d ) fj=(sj-sj-i+mod)%mod fj=(sj−sj−i+mod);否则, f j = s j fj=sj fj=sj。这样就不需要 g g g数组了。
    这里要注意因为s[0]也有存值,如果$j-i<0,不能用s[j]-s[0],这样会把逆序对为0的情况抹掉
cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 1e9 + 7;
int n, c, f[10005], s[10005];

signed main() {
	cin >> n >> c;
	f[0] = 1, f[1] = 0;
	s[0] = 1;
	for (int i = 2; i <= n; i++) {
		int cd = min(c, i * (i - 1) / 2);
		s[0] = f[0];
		for (int j = 1; j <= cd; j++)
			s[j] = (s[j - 1] + f[j]) % mod;
		for (int j = 0; j <= cd; j++) {
			if (j - i >= 0)
				f[j] = ( s[j] - s[j - i] + mod) % mod;
			else
				f[j] = s[j];
		}

	}
	cout << f[c];
	return 0;
}

感叹一下,这题的细节好多...

相关推荐
Navigator_Z7 小时前
LeetCode //C - 1089. Duplicate Zeros
c语言·算法·leetcode
云泽8089 小时前
C++ 可调用对象通关指南:深度解析 Lambda 表达式、function 包装器与 bind 绑定器
开发语言·c++·算法
wlsh1510 小时前
Go 迭代器
算法
语戚10 小时前
力扣 3161. 块放置查询:线段树解法(Java 实现)
java·算法·leetcode·面试·线段树·力扣·
CS创新实验室11 小时前
从顺序表到动态数组:数据结构的永恒基石与现代语言的优雅封装
数据结构·算法
Black蜡笔小新11 小时前
自动化AI算法训练服务器DLTM训推一体化平台助力农业生产管理实现安全智能化
人工智能·算法·自动化
8Qi812 小时前
LeetCode 23. 合并 K 个升序链表 —— 小顶堆(PriorityQueue)
数据结构·算法·leetcode·链表·
QiLinkOS12 小时前
《打破“用爱发电”:一种基于 Gitee 与时间戳的开源权益分配机制探索》
c语言·数据结构·c++·科技·算法·gitee·开源
松间听晚13 小时前
Agentic RL 环境和代码学习:以HGPO为例
算法