[UER #11] 小记

菜的真实场……

T1 切割冰片

发现如果我们决定了竖光的高度(这是一个不降序列),那么横光的状态都可以确定了。

一条条加入横光,DP 式子就是一个前缀和的形式。

于是便有了超简单的 80 分代码(考场脑抽没取 \(\min\) 60 分)。

#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
	char c=getchar();int x=0;
	while(c<48||c>57) c=getchar();
	do x=(x<<1)+(x<<3)+(c^48),c=getchar();
	while(c>=48&&c<=57);
	return x;
}
const int N=503,P=998244353;
int m,n,res;
int a[N];
void inc(int &x,int v){if((x+=v)>=P) x-=P;}
int sum[1000003];
int main(){
	m=read();n=read();
	for(int i=1;i<=n;++i) a[i]=min(read(),m);
	sum[0]=1;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=a[i];++j) inc(sum[j],sum[j-1]);
	for(int i=0;i<=m;++i) inc(res,sum[i]);
	printf("%d\n",res);
	return 0;
}

发现相当于是对一个长度 \(10^9\) 的数组的前若干项做前缀和,然后询问和。

我们考虑快速维护一个结构,满足数列全体求前缀和,询问单点,全体加一个值,便可很好维护上述过程。

考场上我一直在考虑正儿巴经的牛顿多项式怎么维护这个东西,后来发现自己 Naive 了,我们只用维护一个变种牛顿多项式就可以了。

具体的,对于一个多项式函数 \(f\),称它表示的数列为 \(\{f(0),f(1),f(2)\dots\}\),我们要对这个东西求前缀和。

那么构造多项式:

\[f(x)=\sum_{i=0}^{num} a_i{x+i \choose i} \]

前缀和:

\[\begin{aligned} g(x)&=\sum_{i=0}^x f(i)\\ &=\sum_{i=0}^x \sum_{j=0}^{num} a_j{i+j \choose j}\\ &=\sum_{j=0}^{num} a_j \sum_{i=0}^x {i+j \choose j}\\ &=\sum_{j=0}^{num} a_j {x+j+1 \choose j+1}\\ &=\sum_{i=1}^{num+1} a_{i-1} {x+i \choose i} \end{aligned} \]

仅仅是所有的系数右移了一位!我们还可以通过 \(a_0\leftarrow a_0+val\) 来全体加值。

那么我们把序列按值域劈成一段一段,每一段维护上面这样的结构,于是就可以实现快速求前缀和了!

#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
	char c=getchar();int x=0;
	while(c<48||c>57) c=getchar();
	do x=(x<<1)+(x<<3)+(c^48),c=getchar();
	while(c>=48&&c<=57);
	return x;
}
const int N=504,P=998244353;
typedef long long ll;
int m,n;
int a[N],b[N];
int inv[N];
struct poly{
	int coef[N],num;
	void shift(int x){
		for(int i=++num;i;--i) coef[i]=coef[i-1];
		coef[0]=x;
	}
	void init(){coef[num=0]=0;}
	int query(int x){
		int cur=1;
		ll res=0;
		for(int i=0;i<=num;++i){
			res+=(ll)coef[i]*cur%P;
			cur=(ll)cur*inv[i+1]%P*(x+i+1)%P;
		}
		return res%P;
	}
}s[N];
int main(){
	m=read();n=read();inv[1]=1;
	for(int i=1;i<=n;++i) b[i]=a[i]=min(read(),m);
	for(int i=2;i<=n+2;++i) inv[i]=(ll)inv[P%i]*(P-P/i)%P;
	b[n+1]=m;sort(b+1,b+n+2);
	int rk=unique(b+1,b+n+2)-b-1;
	for(int i=1;i<=rk;++i) s[i].init();
	for(int i=1;i<=n;++i){
		int las=1;
		for(int j=1;j<=rk;++j){
			if(a[i]<b[j]) continue;
			s[j].shift(las);
			las=s[j].query(b[j]-b[j-1]-1);
		}
	}
	ll res=1;
	for(int i=1;i<=rk;++i){
		s[i].shift(0);
		res+=s[i].query(b[i]-b[i-1]-1);
	}
	printf("%d\n",int(res%P));
	return 0;
}

T2 科考工作

又是子集和相关问题呢!UOJ 真的好学术。

UNR #6 D2T1 也是一道有趣的子集和问题。

这道题是让我们在 \(2n-1\) 个数中选一个大小为 \(n\) 的子集使得其在模 \(n\) 意义下为 \(0\)。这个问题不经典,所以首先转化下题意。

发现当存在绝对众数时直接输出 \(n\) 个相同的数就可以了,否则通过绝对众数的性质,一定能将下标两两匹配使得每一对的值都不同。

这个只需要考虑归纳,容易知道每次只要选了一个出现次数最多的一定合法。不过实现起来为了保证选出来的值不同代码细节还是挺多的,这里还是建议写堆细节少。

于是我们考虑加强问题的限制,每次只在每一对中选一个,必选落单的那一个,问是否有可能满足条件。

再转化一步,每一对先随便选一个数,然后把换成选另一个数和的差值加入数组,相当于是说有 \(n-1\) 个非 0 的数,问如何选出若干个数使得模意义下和为 \(sum\)

这就是经典的 modular subset 问题了。这类问题如果模数很小可以快速解决。

首先考虑证明解始终存在。事实上,设 \(S_i\) 表示前 \(i\) 个数子集和在模意义下能表示出的集合。我们可以证明,当 \(S_{i-1}\) 不为全集时,\(|S_{i-1}|<|S_i|\),而我们又知道 \(|S_0|=1\),所以 \(S_{n-1}\) 必然为全集。

具体地,反证,考虑若 \(|S_{i-1}|=|S_i|\),那么 \(\forall x\in S_{i-1},x+a_i\in S_{i-1}\),即 \(\forall k\in [0,n-1],x\in S_{i-1},x+ka_i\in S_{i-1}\) 。由于 \(n\) 为质数,\(ka_i\) 在模 \(n\) 意义下构成一个完系。这样的话 \(S_{i-1}\) 为全集,与假设矛盾。

用 OI 的语言来说,我们背包每加入一个数,就必然会有一个位置被置成 1,直接 bitset 90 分。

然而 modular subset 还有更好的性质,由于相当于是将 DP 数组循环位移之后取或,所以位移后 \(0\leftrightarrow 1\) 的对数和 \(1\leftrightarrow 0\) 的对数相同,也就是说只要我们可以找出所有不同的位置,然后只对这些位取或,由上述结论复杂度就是对的了。

至于怎么动态维护 DP 数组并快速找到不同位置,这个东西很经典,用数据结构维护区间哈希然后跳跃就可以了,我写的树状数组+二分双 \(\log\),而估计存在单 \(\log\) 做法。

UPD on 2024.3.15:

zhy 做了这道题并给出了比较简单的单 \(\log\) 构造。假设你当前加入 \(x\),你只需要找到目前不在 modular subset 中的任何一个数 \(v\),然后考虑 \(0\to x\to 2x\bmod n \to 3x\bmod n\to \dots \to v\) 这条链,首是 \(1\) 尾是 \(0\) 所以可以直接二分出一个 \(1\to 0\) 的边。复杂度是单 \(\log\)

同时这道题对任意合数都成立,这个东西被称作 EGZ 定理,只需要考虑 \(n=ab\) 时可以用 \(n=a\) 的构造和 \(n=b\) 的构造拼出来 \(n=ab\) 的构造。具体地,每次找出和为 \(a\) 的倍数的 \(a\) 个数然后将它们删去,直到只剩 \(a-1\) 个数,这样你就有 \(2b-1\) 个组,那么你可以从这些组里找出 \(b\) 个组使其和为 \(ab\) 的倍数。

#include <cstdio>
#include <vector>
#include <algorithm>
#pragma GCC optimize(2,3,"Ofast")
using namespace std;
int read(){
	char c=getchar();int x=0;
	while(c<48||c>57) c=getchar();
	do x=(x<<1)+(x<<3)+(c^48),c=getchar();
	while(c>=48&&c<=57);
	return x;
}
const int N=600003,mod=998244353;
typedef long long ll;
int n,m;
int pw[N],pre[N];
bool f[N],res[N];
struct hashseq{
	int c[N];
	void upd(int x){
		for(int i=x+1;i<=m;i+=(i&-i))
			((c[i]+=pw[x])>=mod)&&(c[i]-=mod);
		for(int i=x+n+1;i<=m;i+=(i&-i))
			((c[i]+=pw[x+n])>=mod)&&(c[i]-=mod);
	}
	int calc(int l,int r){
		int res=0;
		for(int i=r+1;i;i^=(i&-i))
			((res+=c[i])>=mod)&&(res-=mod);
		for(int i=l;i;i^=(i&-i))
			((res-=c[i])<0)&&(res+=mod);
		return res;
	}
}cur;
int a[N],b[N],idx[N],idy[N];
int stk[N],tp;
vector<int> vec[N],lis[N];
int mx,smx;
void pback(int x){
	if(vec[x].empty()) return;
	lis[vec[x].size()].emplace_back(x);
}
int popmax(){
	while(mx&&lis[mx].empty()) --mx;
	int p=lis[mx].back();lis[mx].pop_back();
	int x=vec[p].back();vec[p].pop_back();
	return x;
}
int popsmax(){
	if(smx>mx) smx=mx;
	while(smx&&lis[smx].empty()) --smx;
	int p=lis[smx].back();lis[smx].pop_back();
	int x=vec[p].back();vec[p].pop_back();
	return x;
}
int main(){
	n=read();m=n+n;smx=mx=n-1;pw[0]=1;
	for(int i=1;i<=m;++i) pw[i]=pw[i-1]*2ll%mod;
	for(int i=1;i<m;++i) vec[a[i]=read()].emplace_back(i);
	for(int i=0;i<n;++i){
		if(int(vec[i].size())>=n){
			for(int t=0;t<n;++t) printf("%d ",vec[i][t]);
			putchar('\n');
			return 0;
		}
		pback(i);
	}
	int sum=0;
	for(int i=1;i<n;++i){
		int x=popmax(),y=popsmax();
		if(a[x]>a[y]) swap(x,y);
		pback(a[x]);pback(a[y]);
		idx[i]=x;idy[i]=y;
		b[i]=a[y]-a[x];
		((sum-=a[x])<0)&&(sum+=n);
	}
	int pos=popmax();
    ((sum-=a[pos])<0)&&(sum+=n);
	printf("%d ",pos);
	if(!sum){
		for(int i=1;i<n;++i) printf("%d ",idx[i]);
		putchar('\n');
		return 0;
	}
	cur.upd(0);f[0]=f[n]=1;
	for(int _=1;_<n;++_){
		int x=0,t=b[_];
		while(x<=n){
			int l=x,r=n;
			while(l<r){
				int mid=(l+r)>>1;
				if(cur.calc(x+t,mid+t)%mod==(ll)pw[t]*cur.calc(x,mid)%mod) l=mid+1;
				else r=mid;
			}
			x=l;
			if(x>=n) break;
			if(f[x]&&!f[x+t]) stk[++tp]=x+t;
			++x;
		}
		while(tp){
			int p=stk[tp--];
			if(p>=n) p-=n;
			f[p]=f[p+n]=1;
			pre[p]=_;cur.upd(p);
			if(p==sum){
				while(p){res[pre[p]]=1;((p-=b[pre[p]])<0)&&(p+=n);}
				for(int i=1;i<n;++i)
					if(res[i]) printf("%d ",idy[i]);
					else printf("%d ",idx[i]);
				putchar('\n');
				return 0;
			}
		}
	}
	return 0;
}

upd: 单 \(\log\) 代码。

#include <cstdio>
#include <bitset>
#include <algorithm>
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin)),p1==p2?EOF:*p1++)
using namespace std;
char buf[1<<21],*p1=buf,*p2=buf;
typedef long long ll;
typedef __int128 lll;
int read(){
	int c=getchar();int x=0;
	while(c<48||c>57) c=getchar();
	do x=(x<<1)+(x<<3)+(c^48),c=getchar();
	while(c>=48&&c<=57);
	return x;
}
const int N=300003;
int n;
int a[N<<1],p[N<<1];bool b[N];
int pre[N],las[N],inv[N];
bool res[N];
ll m;
inline int MD(ll x){
	x-=(((lll)x*m)>>64)*n;
	return x>=n?x-n:x;
}
int main(){
	n=read();m=((lll)1<<64)/n;
	for(int i=1;i<n+n;++i) a[i]=read(),p[i]=i;
	sort(p+1,p+n+n,[](int x,int y){return a[x]<a[y];});
	for(int i=1,j=1;i<n+n;i=j){
		while(j<n+n&&a[p[i]]==a[p[j]]) ++j;
		if(j-i>=n){
			for(int t=0;t<n;++t) printf("%d ",p[i+t]);
			putchar('\n');
			return 0;
		}
	}
	int sum=0,t=0;
	for(int i=1;i<=n;++i){sum+=a[p[i]];if(sum>=n) sum-=n;}
	if(sum) sum=n-sum;
	inv[1]=1;
	for(int i=2;i<n;++i) inv[i]=MD((ll)inv[n%i]*(n-n/i));
	b[0]=1;
	for(int i=1;!b[sum]&&i<n;++i){
		int d=a[p[i+n]]-a[p[i]];
		if(d<0) d+=n;
		while(t<n&&b[t]) ++t;
		int l=0,r=MD((ll)t*inv[d]);
		while(l+1<r){
			int mid=(l+r)>>1;
			if(b[MD((ll)mid*d)]) l=mid;
			else r=mid;
		}
		int pos=MD((ll)r*d);
		pre[pos]=i;b[pos]=1;
		if(pos>=d) las[pos]=pos-d;
		else las[pos]=pos-d+n;
	}
	int x=sum;
	while(x){res[pre[x]]=1;x=las[x];}
	for(int i=1;i<=n;++i)
		if(res[i]) printf("%d ",p[i+n]);
		else printf("%d ",p[i]);
	putchar('\n');
	return 0;
}
posted @ 2022-11-21 22:02  yyyyxh  阅读(131)  评论(0编辑  收藏  举报