NOI 2019 题目选做
斗主地
题目描述
解法
首先考虑 \(30\) 分的做法,我们可以设计 \(f[i][j]\) 表示前 \(i\) 轮第 \(j\) 个位置的期望分数,\(g[i][j]\) 表示对于现在这一轮的 \(a\),第一堆取走了 \(i\) 个,第二堆取走了 \(j\) 个的概率,转移很容易写。
结论是:一次函数洗牌之后的期望仍然是一次函数,二次函数洗牌后的期望仍然是二次函数。由于我的数学功底太差,所以并不能证明这个结论。知道这个结论以后我们用 \(dp\) 维护前三项然后插值即可,时间复杂度 \(O(n+9\cdot m)\)
#include <cstdio>
#include <cstring>
const int M = 105;
#define int long long
const int MOD = 998244353;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,q,w,tp,A,B,C,f[5],l[5],r[5],g[5][5],inv[10000005];
void add(int &x,int y) {x=(x+y)%MOD;}
int qkpow(int a,int b)
{
int r=1;
while(b>0)
{
if(b&1) r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
int ask(int x)
{
return (A*x%MOD*x+B*x+C)%MOD;
}
signed main()
{
n=read();m=read();tp=read();
if(tp==1) A=0,B=1,C=0;
if(tp==2) A=1,B=0,C=0;
inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)
inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=1;i<=m;i++)
{
int x=read(),y=n-x;w^=1;
for(int j=1;j<=3;j++)
l[j]=ask(j),r[j]=ask(j+x);
memset(f,0,sizeof f);
memset(g,0,sizeof g);g[0][0]=1;
for(int j=0;j<=x && j<=3;j++)
for(int k=0;k<=y && k<=3;k++)
{
int v=inv[x-j+y-k];
if(j<x) add(g[j+1][k],g[j][k]*v%MOD*(x-j));
if(k<y) add(g[j][k+1],g[j][k]*v%MOD*(y-k));
}
for(int j=1;j<=n && j<=3;j++)
for(int k=1;k<=j;k++)
{
int v=inv[n-j+1];
if(k<=x) add(f[j],l[k]*g[k-1][j-k]%MOD*(x-k+1)%MOD*v);
if(k<=y) add(f[j],r[k]*g[j-k][k-1]%MOD*(y-k+1)%MOD*v);
}
A=((f[3]-2*f[2]+f[1])*inv[2]%MOD+MOD)%MOD;
B=((8*f[2]-5*f[1]-3*f[3])*inv[2]%MOD+MOD)%MOD;
C=((3*f[1]-3*f[2]+f[3])%MOD+MOD)%MOD;
}
q=read();
while(q--) printf("%lld\n",ask(read()));
}
机器人
题目描述
解法
这道题基本上自己做出来了,话说后面的优化和今年的联合省选一模一样啊。
首先考虑表达限制,很容易把问题联系到笛卡尔树,那么我们借助笛卡尔树的结构来考虑问题。那么就是对于笛卡尔树上的每个点,左儿子区间长度和右儿子区间长度差的绝对值 \(\leq 2\)
那么设计区间 \(dp\),设 \(f[l][r][v]\) 表示区间 \([l,r]\) 的最大值是 \(v\),并且子树内满足条件的方案数,\(g[l][r][v]\) 表示最大值 \(\leq v\) 的方案数,那么转移可以先枚举最大值的位置 \(x\)(最多有 \(3\) 个可能的取值),然后划分成两个区间:
时间复杂度 \(O(n^2B)\),可以得到 \(35\) 分的高分。可以发现有效的区间个数只有 \(m=2000\) 个左右,所以把可能的区间预处理出来,然后 \(dp\) 可以做到 \(O(mB)\),得到了 \(50\) 分的高分!
可以用归纳法证明 \(f,g\) 数组都是不超过 \(n\) 次的分段多项式,按照 \(a_i\) 和 \(b_i+1\) 可以分成若干个左闭右开的区间 \([c_i,c_{i+1})\),我们依次处理每个区间,先计算 \(n+1\) 个点值,然后插值计算出在 \(c_{i+1}-1\) 的点值,因为用来描述函数的点的编号是连续的,所以单次插值可以优化到 \(O(n)\),那么总时间复杂度 \(O(mn^2)\)
由于被卡常了只获得了 \(95\) 分。
还有复杂度更为优秀的做法,我们把区间 \([l,r]\) 计算的点值个数限制在 \(r-l\) 级别,然后如果要用就在线插值,具体见 这篇博客
那么复杂度可以做到 \(O(n\cdot (n^2+(n/2)^2\cdot 3^1+(n/4)^2\cdot 3^2+(n/8)^2\cdot 3^3...))=O(n^3)\)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 305;
const int N = 2605;
const int MOD = 1e9+7;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,a[M],b[M],c[M<<1],f[N][M],id[M][M],vis[M][M];
int T,L,inv[M],tmp[M];
int Abs(int x) {return x>0?x:-x;}
void add(int &x,int y) {x=(x+y)%MOD;}
int mul(int x,int y) {return 1ll*x*y%MOD;}
void init(int l,int r)
{
if(id[l][r] || l>r) return ;id[l][r]=++k;
for(int i=l;i<=r;i++) if(Abs(r-i-(i-l))<=2)
init(l,i-1),init(i+1,r);
}
void work(int l,int r)
{
if(vis[l][r] || l>r) return ;
int u=id[l][r];vis[l][r]=1;
for(int i=l;i<=r;i++)
if(Abs(r-i-(i-l))<=2 && a[i]<=c[T] && c[T]<b[i])
{
work(l,i-1);work(i+1,r);
int x=id[l][i-1],y=id[i+1][r];
for(int v=1;v<=L;v++)
add(f[u][v],1ll*f[x][v]*f[y][v-1]%MOD);
}
for(int v=1;v<=L;v++)
add(f[u][v],f[u][v-1]);
}
void lagrange(int l,int r)
{
if(l+n>=r)
{
for(int u=1;u<=k;u++)
f[u][0]=f[u][r-l+1];
return ;
}
tmp[n+1]=1;
for(int i=n;i>=1;i--) tmp[i]=mul(tmp[i+1],r-l-i);
for(int u=1;u<=k;u++) f[u][0]=0;
for(int i=l,zxy=1;i<=l+n;i++)
{
int xs=mul(tmp[i-l+1],zxy);
int dw=mul(inv[l+n-i],inv[i-l]);
if((l+n-i)&1) dw=MOD-dw;xs=mul(xs,dw);
for(int u=1;u<=k;u++)
add(f[u][0],1ll*f[u][i-l+1]*xs%MOD);
zxy=mul(zxy,r-i);
}
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
a[i]=read();b[i]=read()+1;
c[++m]=a[i];c[++m]=b[i];
}
for(int i=0;i<=n+1;i++) f[0][i]=1;
sort(c+1,c+1+m);m=unique(c+1,c+1+m)-c-1;
init(1,n);inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)
inv[i]=mul(inv[MOD%i],MOD-MOD/i);
for(int i=2;i<=n;i++)
inv[i]=mul(inv[i],inv[i-1]);
for(T=1;T<m;T++)
{
L=min(c[T+1]-c[T],n+1);
memset(vis,0,sizeof vis);
for(int l=1;l<=n;l++) for(int r=1;r<=n;r++)
if(id[l][r]) work(l,r);
lagrange(c[T],c[T+1]-1);
for(int u=1;u<=k;u++) for(int v=1;v<=L;v++)
f[u][v]=0;
}
printf("%d\n",f[id[1][n]][0]);
}
I 君的探险
题目描述
1-5
可以把所有点依次 modify
一遍,然后如果某个点的亮暗状态改变,就说明这两个点之间有边,只检测编号比当前点大的点的亮暗状态可以获得 \(\frac{1}{2}\) 的常数。
6-9
可以依次确定所连点二进制的每一位来做到 \(O(n\log n)\) 的复杂度。具体来说,假设现在考虑到了第 \(w\) 位,就把包含 \(2^w\) 的点给取出来点亮,那么一个点和它的相连点在这一位相等就等价于状态是暗,一个点和它的相连点在这一位不同就等价于状态是亮,那么我们把所有点的亮暗状态都检测一遍即可。
这种构造方法特别常见,要注意积累一下。
10-11
由于每个点只会编号比它小的点连边,那么对于单个点我们可以二分一个前缀,使得点亮这个前缀能改变这个点的亮暗状态。使用整体二分的技巧就可以让复杂度为 \(O(n\log n)\)
12-17
拓展 6-9
的方法,我们发现二进制处理之后,每个点得到的值 \(p[u]\) 就表示和 \(u\) 有边的所有点的编号异或和。
由于是树我们可以从叶子往上构造,我们维护一个队列,初始把 1,2...n
都加入队列里。我们取出队首然后判断它和它的 \(p\) 是否有连边(modify+query+modify
的单次检测方法),如果有边就说明是叶子,把它删除,更改父亲的 \(p\),把父亲加入队列中即可。那么最后达到的效果就是从叶子往上删完了整棵树。
18-25
拓展 10-11
的方法,我们发现如果确定了编号的顺序,那么按照同样的方法,向前缀连边数量奇数的点(简称为奇数点),一定会至少确定它的一条边(奇数点也可能确定多条边,偶数点也可能确定边)
主观上感受奇数点的数量有一半,根据题解所说有 \(n/3\),反正一次整体二分能确定很多边。注意我们要把已经确定的边看作"删除"之后再进行以后的整体二分,以防反复确定同样的边(modify
的时候需要手动改状态)
那么就是循环操作,先随机化一个排列,然后跑整体二分,最后把已经连满的点通过 check
操作去除即可。
不会说明正确性,但是主观上特别有道理。
实现
需要数据分治,封装三个 subtask
,分别实现 1-5
,10-11
(可以顺便处理 6-9
),18-25
的方法即可。
void modify(int x);
int query(int x);
void report(int x, int y);
int check(int x);
#include <cstdio>
#include <vector>
#include <iostream>
#include <ctime>
using namespace std;
const int M = 200005;
#define pb push_back
int n,m;
namespace s1
{
int s[M];
void main()
{
for(int i=0;i<n-1;i++)
{
modify(i);
for(int j=i+1;j<n;j++)
{
bool res=query(j);
if(s[j]^res) report(i,j);
s[j]=res;
}
}
}
}
namespace s2
{
void solve(int l,int r,vector<int> p)
{
if(l==r)
{
for(int x:p) report(x,l);
return ;
}
int mid=(l+r)>>1;
vector<int> pl,pr;
for(int i=l;i<=mid;i++) modify(i);
for(int i=mid+1;i<=r;i++)
if(query(i)) pl.pb(i);
for(int x:p)
{
if(query(x)) pl.pb(x);
else pr.pb(x);
}
for(int i=l;i<=mid;i++) modify(i);
solve(l,mid,pl);solve(mid+1,r,pr);
}
void main()
{
solve(0,n-1,vector<int>());
}
}
namespace s3
{
vector<int> o,g[M];int s[M];
void solve(int l,int r,vector<int> p)
{
if(l==r)
{
for(int x:p)
{
report(x,o[l]);
g[o[l]].pb(x);g[x].pb(o[l]);
}
return ;
}
int mid=(l+r)/2;
vector<int> pl,pr;
for(int i=l;i<=mid;i++)
{
modify(o[i]);
for(int x:g[o[i]]) s[x]^=1;
}
for(int i=mid+1;i<=r;i++)
if(s[o[i]]^query(o[i])) pl.pb(o[i]);
for(int x:p)
{
if(s[x]^query(x)) pl.pb(x);
else pr.pb(x);
}
for(int i=l;i<=mid;i++)
{
modify(o[i]);
for(int x:g[o[i]]) s[x]^=1;
}
solve(l,mid,pl);solve(mid+1,r,pr);
}
void main()
{
srand(time(0));
for(int i=0;i<n;i++) o.pb(i);
while(!o.empty())
{
random_shuffle(o.begin(),o.end());
solve(0,o.size()-1,vector<int>());
vector<int> r;
for(int x:o) if(!check(x)) r.pb(x);
o=r;
}
}
}
void explore(int N,int M)
{
n=N;m=M;
if(n<=500) s1::main();
else if(n%10==7 || n%10==8) s2::main();
else s3::main();
}