[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\}\),我们要对这个东西求前缀和。
那么构造多项式:
前缀和:
仅仅是所有的系数右移了一位!我们还可以通过 \(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;
}