加载中…

返回上一页

CSP-S模拟9(简单却失利)

下发文件和题解

今天这个失利啊,是我总是想得太多,每次一些如小集合推大集合的问题总是认为它没有正确性,不敢写.

T3 二分能拿 85 分,而我因为当时着急写了个特判写错了导致挂成了 10 分. 更恼人的是下面这两张图,不需细说,一目了然


只能说我是挂分能手.

A. 最长上升子序列

k=1 的时候一定是从 n1 的降序排列.

k>1 的时候不难发现,在原序列中插值的时候一定是每两个数之间最多只插入一个数(不包括原序列最后一个数以后的部分). 就拿一个例子:

2

5

1

3

4

6

7

假如 25LIS,那么尝试插入至 25 之间,可以插入 167.

要求字典序最小,那么中间一定插 1.

2

1

5

3

4

6

7

如果中间能插多个值,34 一定不行,贪心地尝试插 6. 由于字典序要求最小,插在 1 后面.

2

1

6

5

3

4

7

插入完 6 之后,可以发现 7 无论插在哪里,LIS 都会变,除非插在 1 前面使得字典序变大,那么这种情况就不是最优的了.

为了保证字典序尽可能小,如果当前要插的值不合法的话,我们就把这个值插到下一个位置. 这个同样能够证明,在这里就不过多重复了.

到了最后面,每一个空都过完一遍的时候,把剩下的没有插的值倒过来插,因为剩余的值也是比较大的了,如果正序插就不合法,证明也是和上面一样的,多模几个序列就知道了,不存在更优的情况.

按照这种方法,上面例子生成的序列就是:

2

1

7

6

5

4

3

(其实这个例子不是特别好,因为中间插 1 就已经是最后一个空了,后面的直接全部倒序输出了,原谅我考虑得不周到,领会意思就行了

点击查看代码
#include<bits/stdc++.h>
#define ll int
#define rg register
#define rll rg ll
#define maxn 200001
#define pll pair<ll,ll>
#define put_ putchar(' ')
#define putn putchar('\n')
using namespace std;
static inline ll read()
{
	rll f=0,x=0;rg char ch=getchar();
	while(ch<'0'||ch>'9') f|=(ch=='-'),ch=getchar();
	while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^'0'),ch=getchar();
	return f?-x:x;
}
static inline void write(rll x)
{
	if(x<0) putchar('-'),x=-x;
	if(x>9) write(x/10);putchar(x%10|'0');
}
ll n,k,st;
bool fl[maxn];
ll a[maxn];
int main()
{
	n=read();k=read();if(k==1) { for(rll i=n;i;i--) write(i),put_; return 0; }
	st=1;for(rll i=1;i<=k;i++) a[i]=read();
	for(rll i=1;i<k;i++)
	{
		write(a[i]),put_;fl[a[i]]=1;// 输出一个原序列的数
		while(fl[st]) st++;
		if(st<a[i]) write(st),put_,fl[st]=1;// 插空填补
	}
	for(rll i=n;i;i--) if(!fl[i]) write(i),put_;// 最后倒序输出
	return 0;
}

B. 独特序列

假设一个长度为 m 的子序列 ba 的一个前缀(就是已经处理到的位置,处理到的这个位置即是 bm)中仅出现了一次. 现在要在这个合法的序列后面插入一个元素 ai,那么只需满足从 bm 对应 a 中的位置到 ai 之间的部分不存在 bmai 就是合法的,具体证明可以尝试举反例,用反证法说明不可能存在这种情况,这里不详细说了.

那么可以设 f[i] 为选定 i 后,a 中的前 i 个元素有多少个子序列只出现了一次. 那么要想把 f[i] 的情况转移到 f[j] 上,只有在 ( i , j ) 这个区间内没有 aiaj 才可以.

在不符合情况的时候,要把它删除;否则就要把前面的值都转移过来. 这涉及到了单点修改、区间查询,用一个 bit 维护即可. 记录一个 pre 数组,表示上一个这个数是在哪一个位置出现的. 遇到出现 pre 的情况,就把前面对应位置转移的值删除. 其余直接加上即可.

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define rg register
#define rll rg ll
#define maxn 300001
#define mod 998244353
#define pll pair<ll,ll>
#define put_ putchar(' ')
#define putn putchar('\n')
using namespace std;
static inline ll read()
{
	rll f=0,x=0;rg char ch=getchar();
	while(ch<'0'||ch>'9') f|=ch=='-',ch=getchar();
	while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^'0'),ch=getchar();
	return f?-x:x;
}
static inline void write(rll x)
{
	if(x<0) putchar('-'),x=-x;
	if(x>9) write(x/10);putchar(x%10|'0');
}
template<typename T,size_t sz>
class bit
{
#define lowbit(x) (x&-x)
private:
	T c[sz];
public:
	inline void update(rg T x,rg T n,rg T v) { for(rll i=x;i<=n;i+=lowbit(i)) c[i]+=v; }
	inline T query(rg T x) { rg T ans=0; for(rll i=x;i;i-=lowbit(i)) ans+=c[i]; return ans; }
#undef lowbit
};

ll n,ans;
ll a[maxn],lst[maxn],pre[maxn],nxt[maxn],f[maxn];
bit<ll,maxn> t;
int main()
{
	n=read();
	for(rll i=1;i<=n;i++)
	{
		a[i]=read();
		if(lst[a[i]]) pre[i]=lst[a[i]],nxt[lst[a[i]]]=i;
		lst[a[i]]=i;
	}
	t.update(1,n,1);
	for(rll i=1;i<=n;i++)
	{
		f[i]=(t.query(i)-t.query(pre[i]/*-1+1*/))%mod;
		if(pre[i]) t.update(pre[i]+1,n,-f[pre[i]]);
		if(!nxt[i]) ans=(ans+f[i])%mod;
		t.update(i+1,n,f[i]);
	}
	write(ans);
	return 0;
}

C. 最大GCD

首先,直接二分不对,比如你可以模这个样例:

in=
3 12
3 4 8
ans=9
out=6

在答案大于 A 中的最大值时,二分是正确的,但是也没必要二分. 小于的时候,答案并不是单调的,就比如上边的那个样例.

所以分两种情况讨论:

  1. 答案大于等于 A 中的最大值:

    直接把 k 均摊,让每一个元素的值相同即是答案.

// 预处理
n=read();k=read();for(rll i=1;i<=n;i++) a[i]=read();
sort(a+1,a+n+1);mx=a[n];for(rll i=1;i<=n;i++) sum[i]=sum[i-1]+a[i];
// 特判
if(sum[n]+k>=n*mx) { write((sum[n]+k)/n); return 0; }
  1. 答案小于 A 中的最大值:

    只需要每次枚举答案,由于上面把 A 数组已经排序过了,下面只需要找到每一个数向上加能对应的最小的刚才枚举的答案的倍数,加上差值. 由于是单调的,就没必要每一个数扫了,枚举这个倍数(1、2、3、4、5……),用一个前缀和维护一下一减就是差值.

    复杂度即是 O(ai max log ai max log n ).

for(rll i=mx;i;i--)
{
	rll t=0;
	for(rll j=1;i*(j-1)<mx;j++)
	{
		l=upper_bound(a+1,a+n+1,i*(j-1))-a;r=upper_bound(a+1,a+n+1,i*j)-a-1;
		if(i*j>mx) r=n;
		if(l>r||a[l]>i*j) continue;
		t+=i*j*(r-l+1)-sum[r]+sum[l-1];
	}
	if(t<=k) { ans=i; break; }
}
write(ans);

D. 连续子段

一看 K ≤ 16,很明显是状压 dp. 设 dp[i][j] 表示当前考虑到了 i,当前已选的集合为 j(010110001010瞎打的).

这个指的是选了哪些数,后面来交换用(其实 dp 的时候就把这些次数算上了,不是后面交换).

很明显,有两种情况,一种是选当前数,一种是不选. 因此下面分类讨论.

不选当前位置的这个数的时候,它所需的步数当然是已选点和未选点中点数最少的那个集合,这样移动才能使跨的步数更少.

dp[i][j]=min(dp[i][j],dp[i-1][j]+min(bitcnt(j),bitcnt(j^((1<<k)-1))));

bitcnt 就是已选点的集合,统计二进制下 1 的个数)

当前位置的这个数的时候,由于要把序列从左到右排序,又因为是从左往右统计每一个点的,所以只需要和比当前这个数大的值交换一下就可以了.

具体怎么统计呢?让集合 j 右移 a[i] 就行了. 因为 dp 里面状态是倒着存的嘛.

if((!(j&(1<<a[i]-1))))
		dp[i][j|(1<<a[i]-1)]=min(dp[i][j|(1<<a[i]-1)],dp[i-1][j]+bitcnt(j>>a[i]));

然后把 dp 到的每一个点的全集情况取一个 min 就可以了.

(感谢 kiritokazuto 大佬的悉心指教,是他使我把他 jü jī 了的)

编号 题目 状态 估分 分数 总时间 内存 提交者 提交时间
#286433 #D. 连续子段 Accepted 286432 (100%) -1 100 16734 ms 103668 KiB kiritokazuto 2022-09-22 17:54:54

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define rg register
#define rll rg ll
#define maxn 201
#define put_ putchar(' ')
#define putn putchar('\n')
using namespace std;
static inline ll read()
{
	rll f=0,x=0;rg char ch=getchar();
	while(ch<'0'||ch>'9') f|=ch=='-',ch=getchar();
	while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^'0'),ch=getchar();
	return f?-x:x;
}
static inline void write(rll x)
{
	if(x<0) putchar('-'),x=-x;
	if(x>9) write(x/10);putchar(x%10|'0');
}
ll n,k,ans=LLONG_MAX;
ll a[maxn],s[maxn],cnt[maxn];
ll dp[maxn][1<<16];
ll fl[maxn];
static inline ll bitcnt(rll x) { rll ans=0; while(x) ans+=x&1,x>>=1; return ans; }
int main()
{
	memset(dp,0x3f,sizeof(dp));dp[0][0]=0;
	n=read();k=read();for(rll i=1;i<=n;i++) a[i]=read();
	for(rll i=1;i<=n;i++)
		for(rll j=0;j<1<<k;j++)
		{
			dp[i][j]=min(dp[i][j],dp[i-1][j]+min(bitcnt(j),bitcnt(j^((1<<k)-1))));
			if((!(j&(1<<a[i]-1))))
				dp[i][j|(1<<a[i]-1)]=min(dp[i][j|(1<<a[i]-1)],dp[i-1][j]+bitcnt(j>>a[i]));
		}
	for(rll i=1;i<=n;i++) ans=min(ans,dp[i][(1<<k)-1]);write(ans);
	return 0;
}
posted @ 2022-09-22 17:59  1Liu  阅读(10)  评论(1)    收藏  举报