【比赛】2022.11.24 NOIP模拟赛

A. 不降序列

题目描述

lzx2005 了解到有一种在 \(O(n\log n)\) 的时间复杂度内求出一个序列 \(a\) 的最长不下降子序列的方法如下:

维护一个序列 \(b\),初始时为空。依次考虑 \(a_1,a_2,\ldots ,a_n\),当考虑到 \(a_i\) 时,求出序列 \(b\) 中第一个比 \(a_i\) 大的元素 ,然后使用 \(a_i\) 替代 。特别地,如果不存在这样的元素,那么将 \(a_i\) 加入到序列的末尾。在处理完所有元素后, 得到了一个序列 \(b_1,b_2,\ldots,b_k\),则 \(k\) 即为原序列的最长不降子序列的长度。 不幸的是,lzx2005 把原序列弄丢了,只留下序列 \(b\)lzx2005 只记得原序列的长度为 \(k\),且每个元素都介于 \(1\sim m\) 之间。 lzx2005 希望找到一个满足要求的原序列,或者发现自己算错了。 众所周知,lzx2005 喜欢咕咕咕,请你帮帮他。

输入格式

从文件 lis.in 中读入。

第一行三个正整数 \(n,m,k\),表示原序列的长度、值域以及最长不降子序列长度。

第二行 \(k\) 个空格隔开的整数 ,表示序列 \(b\)

输出格式

输出到文件 lis.out 中。

第一行输出一个字符串,表示是否存在这样的原序列:如果存在,那么输出 Yes,否则输出 No

如果存在这样的原序列,则你还需要输出第二行: \(k\) 个空格隔开的整数 ,表示你找到的原序列。 如果存在多个满足要求的原序列,你可以输出其中任意一个。

样例输入

4 10 3
3 7 7

样例输出

Yes
3 7 10 7

数据范围

对于全部数据,\(1\le k\le n\le 3\times 10^5\)\(1\le m\le 10^9\)\(1\le b_i\le m\),保证 \(b\) 单调不降。

subtask1(20) \(n\le 7,m\le 10\)

subtask2(20) \(n\le 500\)

subtask3(25) \(n\le 5000\)

subtask4(20) \(n\le 10^5\)

subtask5(15) 无特殊限制。

分析

B. 分组问题

问题描述

小 X 的班级有 \(n\) 位学生,第 \(i\) 位学生可以花的时 \(s_i\) 间完成一个课时。小 X 需要把学生们分入若干个学习小组,使得每个学生都在恰好一个学习小组中。

如果同一个小组中学生的学习进度相差过大,那么速度快的学生会产生一定的不满,同时会给速度慢的学生带来压力。因此一个组的不和谐度为这个小组成员间学习速度的极差,也就是小组中最大的 \(s_i\) 与最小的 \(s_i\) 的差。特别地,可能有学生独自一组学习,则这个小组的不和谐度为 \(0\)

小 X 想要知道,一共有多少种不同的分组方式,可以使得分组后所有组的不和谐度之和不超过 \(k\)

但是小 X 并不会,请你帮帮他。因为答案可能很大,所以小 X 只要你求出方案数对 \(10^9+7\) 取模的结果即可。

注: 小 X 认为两个分组方式不同,当且仅当存在一对学生,他们在一种分组方式中在同一组而在另一种方式中不在同一组。

输入格式

从文件 divide.in 中读入。

第一行两个整数 \(n,k\) 表示人数和允许的不和谐程度之和最大值。

第二行 \(n\) 个空格隔开的整数 \(s_i\),表示每个学生的学习速度。

输出格式

输出到文件 divide.out 中。

输出一行一个整数表示方案数对 \(10^9+7\) 取模的结果。

样例输入

4 5
1 3 5 7

样例输出

9

数据范围

对于所有测试数据,\(1\le n,s_i\le 500\)\(0\le k\le 1000\)

子任务 1(20 分):\(n\le 10\)

子任务 2(15 分):\(k\le 2\)

子任务 3(15 分):不同的 \(s_i\) 只有两种。

子任务 4(20 分):\(\sum s_i\le 1000\)

子任务 5(30 分):无特殊限制。

C. 变换问题

问题描述

你有一个数组 \(a_1, a_2, ... , a_n\),每次可以选择一个下标 \(i(1 \le i \le n)\),然后将 \(a_i\) 变成 \(\max(a_i, a_{i - 1}, a_{i + 1})\) 或者 \(\min(a_i, a_{i - 1}, a_{i + 1})\)

注意这里我们认为数组首尾是相邻的,也就是认为 \(a_0\) 表示 \(a_n\)\(a_{n + 1}\) 表示 \(a_1\)

问对于每个 \(k = 1,2,...,m\),需要至少进行多少次操作,能将数组里面所有数字变成 \(k\),如果不行,输出 -1

输入格式

从文件 trans.in 中读入。

第一行两个整数 \(n,m\)

第二行 \(n\) 个正整数 \(a_1, a_2, ... , a_n(1 \le a_i \le m)\),表示整个数组。

输出格式

输出到文件 trans.out 中。

一行 \(m\) 个整数,表示 \(k = 1, 2, ... , m\) 的答案。

样例输入

7 5
2 5 1 1 2 3 2

样例输出

5 5 7 -1 6

数据范围

对于 \(\text{10%}\) 的数据,\(n,m \le 7\)

对于 \(\text{30%}\) 的数据,\(n,m \le 13\)

对于 \(\text{60%}\) 的数据,\(n,m \le 1000\)

对于 \(\text{100%}\) 的数据,\(3 \le n,m \le 2 \times 10^5\)

D. 第四道题

问题描述

Alice和 Bob 进行一个填数的游戏。

Alice 问 Bob 有多少个序列 \(a\),满足 \(a_i\le s\)

Alice 觉得这太简单了,又加了额外 \(m\) 个限制 \((l,r,v)\),每个限制表示 \(a_{l,r}\) 这些数字不能全都和 \(v\) 相同。

答案对 \(998244353\) 取模。

输入格式

从文件 fourth.in 读入。

第一行三个正整数 \(n,m,s\)

接下来 \(m\) 行,每行三个正整数 \(l,r,v\)

输出格式

输出到文件 fourth.out 中。

一行一个整数表示答案。

样例输入

8 8 3
1 5 1
6 8 2
2 6 2
3 7 1
1 3 3
4 8 1
5 6 3
1 8 2

样例输出

5291

数据范围

subtask1(10) \(n,m,s\le 8\)

subtask2(20) \(n\le 2000\)\(m\le 15\)

subtask3(20) \(n,m\le 2000\)

subtask4(10)\(b_i=1\)

subtask5(10) \(b_i\) 两两不同;

subtask6(30) 无特殊限制;

对于 \(100\%\) 的数据,\(n,m,s\le 2\times 10^5\)

官方题解(再附上我的代码,如果订正过的话)

T1 不降序列

首先,如果 \(b\) 不是 \(a\) 的一个子序列,我们可以将其调整为 \(a\) 的子序列,并且答案不会更劣。

那么现在可以看成是在 \(b\) 的基础上扩展,显然,这个问题可以看成最大化扩展出的序列 \(a\) 的长度。 如果我们把数插到中间,显然把这个数拉到后面或者前面不会更劣(这个数比较大的时候拉到前面,比较小的时候拉到后面)。 但是我们发现,比较小的数拉到后面会改变 \(b\) 的值,这不太行,所以我们就直接在开头加数。 显然,最优策略就是从 \(m\) 一直往下加,并且在加的时候要保证不扩大 \(k\) 的值。 每次二分一下就可以知道每个数能加多少个了。 时间复杂度 \(O(n\log n)\)

#include<bits/stdc++.h>
#define N 300005
using namespace std;
int read(){
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
int n,m,k,tot;
int a[N],b[N],B[N],cnt[N],sum[N];
int main(){
	freopen("lis.in","r",stdin);
	freopen("lis.out","w",stdout);
	n=read(),m=read(),tot=k=read();
	for(int i=1;i<=k;++i) B[i]=b[i]=read();
	tot=unique(B+1,B+1+tot)-B-1;
	for(int i=1;i<=k;++i){
		b[i]=lower_bound(B+1,B+1+tot,b[i])-B,cnt[b[i]]++;
	}
	for(int i=1;i<=tot;++i) sum[i]=sum[i-1]+cnt[i];
	B[tot+1]=1e9+1;
	int v=m,pos=1;
	while(pos<=n-k&&v>B[1]){
		int x=lower_bound(B+1,B+2+tot,v)-B;
		if(v<=B[x]) x--;
		for(int i=pos;i<=min(pos+sum[x]-1,n-k);++i) a[i]=v;
		v--;
		pos=pos+sum[x];
	}
	if(pos<=n-k){return printf("No\n"),0;}
	printf("Yes\n"); 
	for(int i=1;i<=k;++i) a[n-k+i]=B[b[i]];
	for(int i=1;i<=n;++i) printf("%d ",a[i]);printf("\n");
	fclose(stdin);
	fclose(stdout);
	return 0;
} 

T2 分组问题

为了避免变量重名,题面里的 \(k\) 下面用 \(m\) 代替。

把题目给出的 数组看成数轴上的 \(n\) 个点,坐标从 \(s_1\) 一直到 \(s_n\)\(s\)需事先排序)。

可以将原问题模型转化为:在数轴上选出若干条线段(可以退化成点),线段端点必须为给定的 \(n\) 个点之一,一个点只能作为一个端点,线段长之和不超过 \(m\),并为每个未作为端点的点,都选出一条包含它的线段的方案数。

于是我们有一个 DP: \(f_{i,j,k}\) 表示就前 \(i\) 个点,让 \((s_i,s_{i+1})\) 被线段覆盖了 \(j\) 次并且截止到线段 \(s_i\) 的总长为 \(k\) 的方案数。

不难发现, \(f_{i,j,k}\) 总是转移到 \(f_{i+1,,k+(s_{i+1}-s_i)\times j}\)(即把 \(s_{i+1}-s_i\) 被覆盖的次数乘上长度计入 \(k\))。下面设 \(d=k+(s_{i+1}-s_i)\times j\)

转移有下面几种情况:

(1)\(i+1\) 不作为端点,这时需要决定 \(i+1\) 属于哪条线段:

\[f_{i+1,j,d}+=f_{i,j,k}\times j \]

(2) 为新建的一条退化成点的线段:

\[f_{i+1,j,d}+=f_{i,j,k} \]

(3) 作为左端点,即新建一条线段:

\[f_{i+1,j+1,d}+=f_{i,j,k} \]

(4) 作为右端点,这时需要决定 \(i+1\) 属于哪条线段:

\[f_{i+1,j-1,d}+=f_{i,j,k}\times j \]

答案为 \(\sum\limits_{k=0}^m f_{n,0,k}\)

时间复杂度 \(O(n^2m)\)

#include<bits/stdc++.h>
#define mod 1000000007
#define int long long
#define N 505
#define K 1005
using namespace std;
int read(){
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
int n,m,ans;
int a[N];
int f[2][N][K];
void add(int &a,int b){
	a+=b;a%=mod;
}
signed main(){
//	freopen("divide.in","r",stdin);
//	freopen("divide.out","w",stdout);
	n=read(),m=read();
	for(int i=1;i<=n;++i) a[i]=read();
	sort(a+1,a+1+n);
	f[0][0][0]=1;
	for(int i=0;i<n;++i){
		int now=i&1;
		for(int j=0;j<=i+1&&(j<<1)<=n;++j)
			for(int k=0;k<=m;++k) f[now^1][j][k]=0;
		for(int j=0;j<=i&&(j<<1)<=n;++j){
			for(int k=0;k<=m;++k){
				int d=(a[i+1]-a[i])*j+k;
				if(d>m) break;
				add(f[now^1][j][d],f[now][j][k]*j);  //i+1 不作为端点 
				add(f[now^1][j+1][d],f[now][j][k]);  //自成一组 
				add(f[now^1][j][d],f[now][j][k]);  //作为左端点 
				if(j>=1) add(f[now^1][j-1][d],f[now][j][k]*j);  //作为右端点 
			}
		}
	}
	for(int i=0;i<=m;++i) add(ans,f[n&1][0][i]);
	printf("%lld\n",ans); 
//	fclose(stdin);
//	fclose(stdout);
	return 0;
} 

T3 变换问题

对于每一个 \(k\),我们把序列中大于 \(k\) 的数看作 1,等于 \(k\) 的数看成 0,小于 \(k\) 的数看成 -1

不难发现被 \(0\) 隔开的段是相互独立的。

对于 0 -1 -1 或者 0 1 1 这样的三元组,只需要一次操作就可以使中间的数变成 0。而对于 0 1 -1 这样的三元组,还额外需要一次操作才能使中间的数变成 0

进一步观察发现,如果存在 1 -1 1 这样 -11 交错的三元组,我们可以通过先对中间的数进行一次操作来使额外的两次操作减少到一次。

容易发现,额外的操作次数实际上可以这样计算:把原序列划分成若干段极长的 -11 交错的段,额外的操作次数就是这些段的长度除以二下取整的总和。

考虑顺序枚举 \(k\),用 set 维护 -11 交错的极长段,时间复杂度 \(\mathcal O(n \log n)\)

只会60分的暴力,QWQ。

T4 第四道题

首先如果存在两个限制 \((l_1, r_1, v_1)\)\((l_2, r_2, v_2)\) 满足 \(l_1 \le l_2\), \(r_2 \le r_1\), \(v_1 = v_2\),那么把 \((l_1, r_1, v_1)\) 扔了。 那么现在相同颜色的限制不存在区间包含关系了。

我们记 \(g_i\) 表示满足所有右端点小于等于 \(i\) 的限制,\(a_1 \sim a_i\) 的取值方案数。 先令 \(g_i = g_{i-1} \times s\),然后我们要减去不满足 \(r = i\) 的限制的方案数。 我们枚举限制 \((l, i, v)\)。如果不满足这个限制,那么 \(a_l \sim a_i\) 都是 \(v\)。 那么 \(g_i−= g[l − 1]\)

发现若存在两个限制的区间相交且颜色相同,那么这两个限制是可以同时不满足的。也就是说如果仅仅按上述减去不合法的方案的话,这两个限制同时不满足的方案数会被多减。 实际上我们要容斥,即枚举集合 \(S\)\(S\) 是一些限制组成的集合,满足这些限制区间的并集是一 个区间且全部同色,且包含 \((l, i, v)\) 这个限制)。 然后 \(g[i]+= (-1) ^{|S|} × w[S]\)。 其中 \(w[S]\) 是集合 \(S\) 的权值,即 \(S\) 中的限制同时不满足的方案数。具体地,若 \(S\) 中最小的左端点为 \(x\),那么 \(w[S] = g[x - 1]\)。 考虑怎么算 \(\sum(-1)^{|S|} × w[S]\)

\(f[v][i][r]\) 为满足以下条件 \(S\)\(\sum(-1)^ {|S|} × w[S]\)

  1. 将所有限制按右端点升序排,\(S\) 由排序之后的前 \(i\) 个限制中的若干个组成。
  2. \(S\) 中的限制的右端点均小于等于 \(r\)
  3. \(S\) 中的限制的颜色均为 \(v\)。 显然有递推式:\(f[v][i][r] = w[{i}] - \sum\limits ^{r-1} _{j=l} f[v][i - 1][j]\)

对每种颜色开一棵动态开点线段树即可维护。 假设 \(n, m, s\) 同阶,时间复杂度 \(O(n\log n)\)

不会,QWQ。

posted @ 2023-05-21 22:22  Aurora-JC  阅读(128)  评论(0编辑  收藏  举报