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

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

题目大意

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

题解

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

代码

#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;
}

自我小结

  • 这题细节较多,同一部分容易想到各种不同做法,但往往有许多存在漏洞,因此改题时耽误了不少时间。
  • 同时也容易陷入思维死循环,忽视最简单的做法,若能捋清思路,可以大大提高效率,减小时间浪费。
posted @ 2021-03-02 21:15  AnAn_119  阅读(113)  评论(0编辑  收藏  举报