JZOJ 6652. 【2020.05.27省选模拟】序列(贪心+序列翻转)

本文介绍了一个关于序列构造的问题,通过从后向前递推的方式解决排列组合问题,同时给出了求解字典序最小方案的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JZOJ 6652. 【2020.05.27省选模拟】序列

题目大意

  • 问给出的NNNMMM的排列,按从头到尾依次加到序列首或尾的规则,共同能得到的新排列的个数,并给出字典序最小的方案。
  • 询问有多组。
  • T≤50,N,M≤1000,∑m≤5000T\le50,N,M\le1000,\sum m\le5000T50N,M1000m5000

题解

  • 先加入队列的数位置不好确定,但最后加入的数一定只能再两端,不妨考虑从后往前推。
  • 这样一来每个时刻已经构成的排列是一段前缀和一段后缀,记录指针Li,RiL_i,R_iLi,Ri表示每个排序前端和后端添加到了新排列的哪个位置,令新排列为AAA,注意对每个旧排列而言,最终得到的新排列AAA的唯一的。
  • 从后依次给每一个排列加入一个数aaa(即从后往前一列一列地加),分四种情况:
  • 1、若A[Li]A[L_i]A[Li]A[Ri]A[R_i]A[Ri]均为000,则加入LiL_iLi并将LiL_iLi右移,答案乘222;(因为此时加入右边也可以,但只考虑一半的情况)
  • 2、若A[Li]=aA[L_i]=aA[Li]=aA[Ri]=aA[R_i]=aA[Ri]=a,则直接移动对应的指针;
  • 3、若A[Li]=0A[L_i]=0A[Li]=0A[Ri]=0A[R_i]=0A[Ri]=0,则添加aaa后移动对应的指针;
  • 4、否则一定不合法,答案为000
  • 这样便能得到方案数。
  • 接着需要构造方案。
  • 每一列数加完后,若此刻所有LiL_iLi相等且所有RiR_iRi相等,即构成了一段可以翻转的区间,把它们记录下来,这种翻转是用来调整使字典序最小的。
  • 最终可以得到一连串相互包含的这样的区间,从内往外枚举它们,时刻满足当前字典序最小,有两种情况:
  • 1、若区间iii与区间i+1i+1i+1右端点重合(左端点不可能重合,因为在上面的情况111选择加入了左端点),当且仅当A[p[i+1].l]<A[p[i].l]A[p[i+1].l]<A[p[i].l]A[p[i+1].l]<A[p[i].l]时,翻转区间i+1i+1i+1,再翻转区间iii,即把A[p[i+1].l]A[p[i+1].l]A[p[i+1].l]调整到当前最左边;
  • 2、否则若iii区间左端点大于右端点,则翻转区间i+1i+1i+1,则翻转区间iii,因为要把i+1i+1i+1更小的一侧转向右边,再随iii一起翻回来。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 1010
#define md 1000000007
int a[N][N], A[N], L[N], R[N], st[N][2];
int read() {
	int s = 0;
	char x = getchar();
	while(x < '0' || x > '9') x = getchar();
	while(x >= '0' && x <= '9') s = s * 10 + x - 48, x = getchar();
	return s;
}
void turn(int x) {
	int s = st[x][0] + st[x][1];
	for(int i = st[x][0]; i < s - i; i++) swap(A[i], A[s - i]);
}
int main() {
	int tn = read();
	while(tn--) {
		int n = read(), m = read(), i, j;
		memset(A, 0, sizeof(A));
		for(i = 1; i <= n; i++) {
			for(j = 1; j <= m; j++) scanf("%d", &a[i][j]);
			L[i] = 1, R[i] = m;
		}
		int tot = 1, s = 1;
		st[1][0] = 1, st[1][1] = m;
		for(j = m; j && s; j--) {
			for(i = 1; i <= n; i++) {
				if(!A[L[i]] && !A[R[i]]) s = s * ((L[i] != R[i]) + 1) % md, A[L[i]++] = a[i][j];
				else if(A[L[i]] == a[i][j]) L[i]++;
				else if(A[R[i]] == a[i][j]) R[i]--;
				else if(A[L[i]] != a[i][j] && A[R[i]] == 0) A[R[i]--] = a[i][j];
				else if(A[R[i]] != a[i][j] && A[L[i]] == 0) A[L[i]++] = a[i][j];
				else {
					s = 0;
					break;
				}
			}
			int ok = 1;
			for(i = 2; i <= n && ok; i++) if((L[i] != L[i - 1] || R[i] != R[i - 1])) ok = 0;
			if(ok) st[++tot][0] = L[1], st[tot][1] = R[1];
		}
		printf("%d\n", s);
		if(s) {
			for(j = tot; j; j--) {
				if(j < tot && st[j][1] == st[j + 1][1]) {
					if(A[st[j][0]] > A[st[j + 1][0]]) turn(j + 1), turn(j);
				}
				else {
					if(st[j][0] < st[j][1] && A[st[j][0]] > A[st[j][1]]) {
						if(j < tot) turn(j + 1);
						turn(j);
					}	
				}
			}
			for(i = 1; i <= m; i++) printf("%d ", A[i]);
			puts("");
		}
	}
	return 0;
}

自我小结

  • 这题细节较多,同一部分容易想到各种不同做法,但往往有许多存在漏洞,因此改题时耽误了不少时间。
  • 同时也容易陷入思维死循环,忽视最简单的做法,若能捋清思路,可以大大提高效率,减小时间浪费。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值