P3600 随机数生成器
霜风凄紧,关河冷落,残照当楼。——题记
题意
-
给定真随机序列
a_n ,其中a[i] \in [1,x] 。 -
给出
q 次操作,每次求result[i]=\min_{j=l[i]}^{r[i]} a[j] 。 -
定义
Ans=\max_{i=1}^{q} result[i] 。 -
求
E(Ans) 。
数据范围
解析
0.区间整理
-
显而易见,如果区间
a\subseteq b,b 对最后的Ans一定无贡献(因为a 的结果只可能等于或者优于它的)。 -
所以我们删去所有包含了其他区间的区间。
-
余下的区间要么相离,要么相切,要么相交,但一定不包含。
-
将余下的区间按照
l[i] 升序排序,由上可知,其r[i] 同样构成升序关系,也就是所有区间严格构成了升序关系。
1.转化题意
-
其中
Cases(Ans=i)= Cases(Ans\leqslant i)-Cases(Ans\leqslant i-1) 。 -
记当前希望达到的
\leqslant Ans 为\leqslant T ,\leqslant T 的数字为对勾,显而易见每一个区间都需要有至少一个对勾。
2.设计 dp 状态
-
-
定义
L[i],R[i] 为包含i 的最左/最右区间的编号,res[T] 为\leqslant T 的总方案数,u 为有意义区间。 -
显然有
dp[0]=1,dp[i] \to dp[i+1 \to i'] ,其中i' 满足以下条件:R[i]+1 \geqslant L[i'] (否则从i 到i' 一定跳过了一个区间R[i]+1 ,使得它内部没有对勾)。容易处理出to[i] 表示i 对应的转移右端点。 -
那么可以外层枚举
T ,于是有for(k=i+1 \to to[i]) \; dp[k]+=dp[i]* T* (x-T)^{k-i-1} -
经过了两次写单调队列优化的笔记(现在是
7.13 我回来看的时候发现有 bug 在修),我们其实可以看出,把这个死难看的式子化成一般的 dp 转移方程之后好像可以前缀和优化到内层O(n) 。即(括号内部为\sum 所属):
-
需要注意的一个小细节:
res[T]\ne dp[n], 因为dp[i](u[cnt].l \leqslant i < n) 的状态都可以选择后面还空着的位置乱放(对钩或非对钩其实都行),也对res[n] 有贡献。 -
我们考虑一下怎么不重不漏地计算它。
-
如果点
i 有这么一种操作:在j(j<n) 处又放了一个对钩。则会被上面的转移式计算上,因为转移顺序正确,这种操作可以合并到j 处来考虑,没问题。 -
如果点
i 有这么一种操作:在n 处放一个对钩。则会被转移式计算上,而且不再有后效性, OK 了。 -
如果点
i 有这么一种操作:一直不放对钩。可以看出,由于我们的 dp 状态设计为“在i 处恰好放了一个对钩”,这种操作是上面的转移无法统计的。我们应当手动补一下这种转移,即dp[n]+=\sum\limits_{i=u[cnt].l}^{n-1} dp[i]* (x-T)^{n-i} 。
-
-
好在正解的转移和这个没关系。
-
显然内层为
O(n^2) ,外层O(n) ,合计O(n^3) 。据说可以卡过去,不过至少我的版本不行(大概吧)(主要我的版本写的bug比较多,没有拿到该拿的70分,现在也不想调,就不贴了)。 -
前缀和优化也许能过,但各项不一样的权(系数)实在是烦人得很。而且这个优化是我回来修订的时候才想到的,所以我们继续看正解吧。
3.dp 优化
-
观察到
T 起到的作用是转移系数,它的枚举好像不那么必须。 -
不枚举
T ,只考虑放对号的问题。定义dp[i][j] 为最后一个对勾放在了i ,包含i 的最后一个区间以左(含)的所有区间都有了至少一个对勾,当前一共放了j 个对勾的方案数。for(k=i+1 \to to[i]) \; dp[k][j+1]+=dp[i][j] -
那么对于T的情况,
res[T]= \sum\limits_{j=0}^n (\sum\limits_{i=u[cnt].l}^n dp[i][j])* T^j* (x-T)^{n-j},O(n^2) -
注意,这里之所以没有特判是因为我们这个式子和上面殊途同归(把那一部分都暴力乘进去了,
(x-T)^{n-j} 这一项包括了最后几个位置放非对钩的操作)。 -
考虑
dp 的转移。乍一看似乎两维+转移O(n^3) ,但仔细观察转移会发现,转移到的一定是连续的一段(见上)。所以可以使用差分优化dp ,这样转移就是O(1) ,合计O(n^2) 。(当然也可以前缀和优化,而且我们看到这里的前缀和没有系数!) -
总复杂度
O(Tn) ,下面是代码(推导性注释已删去,调试代码已删去,仅保留了少量的解释性注释。另保留了原本的致命错误(以注释的形式,方便下面说))
4.再谈前缀和优化的枚举T 一维 dp与本题的加强版
-
和教练交流了一下,这种做法是对的,这是肯定的。收尾部分的特判也可以前缀和解决。但我们可以讨论一点推广的东西。
-
本题
x,n 同阶,我们考虑一下当x>>n (也即T>>n )时本题是否还可解。 -
可以!
-
我们首先考虑一维 dp 方案:这里的复杂度瓶颈在于外层的
O(T) 。但我们注意到,不管T 是多少,不影响内部转移逻辑,只影响内部转移系数。大胆猜测这(res[T] ,即T 对应的dp[n] )是个多项式,从而它稍微变换一下之后的前缀和(\sum\limits_{T=1}^xT* res[T]=ANS )也是个多项式!求出前不知道多少项的dp[n] ,然后把整个的前缀和插出来! -
然后讨论二维 dp 方案:观察
res[T] 的式子,可以发现里面的\sum\limits_{i=u[cnt].l}^n dp[i][j] ,也即涉及转移的部分与T 无关而恒定,T 还是系数!则可以视之为关于T 的多项式,插!(而且到了这里两种做法的res[T] 是同一个,不可能一个是多项式一个不是)后同上。 -
考虑一个更强的限制:某些地方的随机是在一个限制的范围内(小于其他地方的范围)。
-
可以!
-
考虑用这些限制把整个
T 的枚举过程分成一些段。举个例子,有如下限制(在哪里限制先不去谈):[1,1000],[1050,2000],[500,1500] ,那么我们可以把枚举的T 分成[1,499],[500,1000],[1001,1049],[1050,1500],[1501,2000] 这么几段。 -
每一段的 dp 转移完全一样!所以对于每一段,把开头的一点暴力 dp 出来,然后本段的和可以前缀和拉插!
-
不过这样就只能用枚举
T 的一维 dp 了(转移不一样,没法保证一个一直不变的\sum\limits_{i=u[cnt].l}^n dp[i][j] )。
#include<bits/stdc++.h>
#define ll long long
#define us unsigned
#define b_s basic_string
#define For(i,a,b) for(int i=(a);i<=(b);++i)
#define foR(i,a,b) for(int i=(a);i>=(b);--i)
using namespace std;
ll rd(){
ll s=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){s=(s<<3)+(s<<1)+(ch^48);if(ch!='\n') ch=getchar();}
return s*f;
}
char wtst[66];
int wtps;
void wt(ll x){
if(x<0) putchar('-'),x=-x;
while(x>9) wtst[++wtps]=(x%10+'0'),x/=10;
wtst[++wtps]=x+'0';
while(wtps) putchar(wtst[wtps--]);
return;
}
void wth(ll x){wt(x);putchar('\n');return;}
void wtk(ll x){wt(x);putchar(' ');return;}
const int maxn=2e3+7,mod=666623333;
int n,x,q,cnt,L[maxn],R[maxn],to[maxn];
bool fei[maxn];
struct space{
int l,r;
}a[maxn],u[maxn];
bool cmpsp(space x,space y){return x.l!=y.l?x.l<y.l:x.r<y.r;}
int tp[maxn][maxn],dp[maxn][maxn],res[maxn],ans[maxn],rans[maxn],ANS;
void pre(){
n=rd(),x=rd(),q=rd();
For(i,1,q) a[i].l=rd(),a[i].r=rd();
//去除包含其他区间的区间,并保证区间有序性
sort(a+1,a+1+q,cmpsp);
// int p=1,ln=a[1].l,rn=a[1].r;
// For(i,2,q){
// if(a[i].l<=ln && a[i].r>=rn) fei[i]=1;
// else if(a[i].l>=ln && a[i].r<=rn) fei[p]=1,p=i,ln=a[i].l,rn=a[i].r;
// else p=i,ln=a[i].l,rn=a[i].r;
// }//bug 1,效果是不能正确地删掉区间
//(反例:[1,7] [2,8] [3,4] 上面那种做法会bug (ln=1,rn=7->ln=2,rn=8->删掉[2,8]并且ln=3,rn=4,但不会继续回溯删掉[1,7]))
for(int i=1;i<=q;i++){
for(int j=1;j<=q;j++){
if(i!=j){
if(a[i].l==a[j].l&&a[i].r==a[j].r&&(fei[i]==1||fei[j]==1)) continue;
if(a[i].l<=a[j].l&&a[i].r>=a[j].r){
fei[i]=1;
}
}
}
}
For(i,1,q)
if(!fei[i])
++cnt,u[cnt].l=a[i].l,u[cnt].r=a[i].r;
//处理L与R
int LN=1,RN=0;
For(i,1,n){
while(LN<=cnt && u[LN].r<i) ++LN;//bug 2(<=cnt),效果是使得在最右一个区间右边的点的L趋于无穷大以至于无法向那些点转移(有理由认为这本来应当造成越界访问...)
//注意必须是<=cnt而非+1<=cnt,因为L随右,它们的L是那个不存在的"cnt+1"
while(RN+1<=cnt && u[RN+1].l<=i) ++RN;
L[i]=LN,R[i]=RN;
}
//处理转移的右端点
int ton=1;
For(i,0,n-1){
while(ton<=n+1 && R[i]+1>=L[ton]) ++ton;
--ton;
to[i]=ton;
}
to[n]=n+1;//bug 3,效果是dp[n][j]不断给tp[1][j+1]扔负数(因为to[n]=0)
return;
}
void DP(){
dp[0][0]=1;//我们的DP数组是差分意义下的。只要保证每次转移以前他们已经被改成正常的就好
For(i,0,n)
For(j,0,i){
if(i>0) tp[i][j]=(tp[i][j]+tp[i-1][j])%mod;
dp[i][j]=(dp[i][j]+tp[i][j])%mod;
tp[i+1][j+1]=((ll)tp[i+1][j+1]+dp[i][j])%mod;
tp[to[i]+1][j+1]=((ll)tp[to[i]+1][j+1]-dp[i][j]+mod)%mod;
}
For(j,1,n)
foR(i,n,0){//u[cnt].l,u[cnt].r) //bug 4,效果是在最右一个区间右边还有点的时候在统计res[j]时忽略那些点
if(R[i]<cnt) break;
res[j]=((ll)res[j]+dp[i][j])%mod;//res:放j个的实际方案数
}
return;
}
int qpow(int x,int t){
int ret=1;
while(t){
if(t&1) ret=(ll)ret*x%mod;
x=(ll)x*x%mod;
t>>=1;
}
return ret;
}
int inv(int x){return qpow(x,mod-2);}
void sol(){
For(T,1,x){
For(j,1,n)
ans[T]=((ll)ans[T]+(ll)res[j]*qpow(T,j)%mod*qpow(x-T,n-j)%mod)%mod;//ans:<=T的实际方案数
rans[T]=((ll)ans[T]-ans[T-1]+mod)%mod;//rans:==T的实际方案数
ANS=((ll)ANS+(ll)rans[T]*T%mod)%mod;//ANS:总期望之和
}
ANS=(ll)ANS*inv(qpow(x,n))%mod;//ANS在这里就变成全期望/全组合=真期望了
wt(ANS);
}
int main(){
pre();
DP();
sol();
return 0;
}
总结与反思
1.知识点
-
学到了差分DP的写法(简单来说核心思路就是转移时对转移终点进行差分性转移,而因为
for 是有序的,可以在枚举起点的同时进行前缀和操作,于是可以保证作为转移起点的数组处在非差分状态)- 顺便说一下
tp 数组的意义。看起来很像一个不必要的中间转存数组,对吧?答案是:就是没用。另一种更简洁的写法是这样的:if(i>0 && j>0) dp[i][j]=((ll)dp[i][j]+dp[i-1][j])%mod; dp[i+1][j+1]=((ll)dp[i+1][j+1]+dp[i][j])%mod; dp[to[i]+1][j+1]=((ll)dp[to[i]+1][j+1]-dp[i][j]+mod)%mod; - 注意到其中有一个额外的
j>0 的判定,因为dp[i(i>0)][0] 显然= 0 。当时其实很早就已经把这个j>0 的错查出来了,但后来怎么都调不出来,和WHM对拍的时候用了tp 。
- 顺便说一下
-
学到了二分调代码的方式,尽管是不光彩的拼接式对拍...
-
明白了了期望-概率-组合的转化关系,意识到期望题的有力解法之一就是转成
E=\sum\limits P* V=\dfrac {\sum\limits Cases* V}{U} 求解(P :某情形的概率Cases :某情形出现的方案数V :某情形的贡献/价值U :全集(全方案数))。 -
明白了设计
dp 状态时,\leqslant x 的状态远比=x 的状态好用的多。 -
update:学到了拉插优化 dp ...
2.解题中的失误
-
看上面代码中的四个bug吧。
-
真的非常致命。希望下次能规避掉这种低级而可怕的错误。
3.其他
-
提升调代码能力。这份调了至少
6h 。 -
提升造样例能力。
后记
-
这是我至今为止(
2022.5.14 )调过最难调的代码。其间心态非常崩溃。 -
收获很多,但至少我终于调过时,我只感到疲惫,与......
-
霜风凄紧,关河冷落,残照当楼。