冲刺NOIP2024专题之dp专题
冲刺NOIP2024专题之dp专题
Helping People
看似是期望题,实则暴力跑出概率即可,帮助大家理解期望的本质了属于是
首先要知道期望的最大值不等于最大值的期望,所以直接建线段树是不行的,比如说位置2原本的数很小,但是有很小概率变得极大,那么先算期望再取\(max\)显然会忽略位置2带来的贡献
由于每次只加一,\(q\)的值又很小,所以尝试从原序列最大值开始枚举可能的答案,计算概率,先设 \(dp_{i}\) 表示 \(maxlast \le maxnum+i\) 的概率
我们注意到题面中对修改的限制条件,即要么覆盖,要么不交,再加上\(q\)的值很小,发现可以尝试对每个不交的区间分别操作,之后肆意合并即可,对于包含的区间,可以发现只要计算出来内部区间,使他们分别满足 \(maxlast \le maxnum+i\) 即可计算大区间的 \(dp\) 值,这是一个树形结构啊,直接建树即可
设当前节点加一概率为\(p\)
根据我们上面提到的计算方式,易得转移方程(\(now\)是当前结点编号)
接着统计当前结点的贡献
记得当 \(i \le maxnow\) 时把 \(dp_{i,now}\)赋成\(1\),\(i = maxnow+1\) 时 \(dp_{i,now}= \prod_{j\in son(now)}dp_{i,j} \times (1-p) + p\)
时空复杂度 \(O(q^2)\) 具体实现可以看代码
CODE
#include<bits/stdc++.h>
using namespace std;
long long n,q,a[100100],ST[100100][20][2],lb[100100],fa[5010],nxt[5010],head[5010],stand;
double ans[5010][10010],last=0.0,output;
long long check(int beg,int end){return max(ST[beg][lb[end-beg+1]][1],ST[end][lb[end-beg+1]][0]);}
struct node
{
long long l,r,num;
double p;
}zone[5010];
bool cmp(node o,node v)
{
if(o.l==v.l)
return o.r>v.r;
return o.l<v.l;
}
void build()
{
zone[0].l=1,zone[0].r=n;
long long is=0;
for(int i=1;i<=q;i++)
{
while(zone[i].l>zone[is].r)
is=fa[is];
fa[i]=is,nxt[i]=head[is],head[is]=i,is=i;
}
}
void solve(int now)
{
for(int i=head[now];i;i=nxt[i])
solve(i);
double mid;
if(now==0)
{
for(int j=0;j<=10000;j++)
{
mid=1.0;
for(int i=head[now];i;i=nxt[i])
mid*=ans[i][j];
ans[now][j]=mid;
}
return;
}
for(int j=0;j<=10000;j++)
{
mid=1.0;
for(int i=head[now];i;i=nxt[i])
mid*=ans[i][j];
if(j>=zone[now].num+5000-stand)
ans[now][j]+=mid*(1.0-zone[now].p);
if(j>=zone[now].num+5000-stand)
ans[now][j+1]+=zone[now].p*mid;
}
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
scanf("%lld%lld",&n,&q);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]),ST[i][0][1]=ST[i][0][0]=a[i];
for(int i=1,mid=-1;i<=n;i++)
{
if(1<<(mid+1)==i) mid++;
lb[i]=mid;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=lb[i];j++)
ST[i][j][0]=max(ST[i][j-1][0],ST[i-(1<<(j-1))][j-1][0]);
for(int i=n;i>=1;i--)
for(int j=1;j<=lb[n-i+1];j++)
ST[i][j][1]=max(ST[i][j-1][1],ST[i+(1<<(j-1))][j-1][1]);
for(int i=1;i<=q;i++)
scanf("%lld%lld%lf",&zone[i].l,&zone[i].r,&zone[i].p),zone[i].num=check(zone[i].l,zone[i].r);
sort(zone+1,zone+1+q,cmp);
build();
stand=check(1,n);
solve(0);
for(int i=5000;i<=10000;i++)
{
output+=(ans[0][i]-last)*(double)(stand+i-5000);
last=ans[0][i];
}
printf("%.9lf\n",output);
return 0;
}
Birds
水题,被大家爆切
具体来说就是背包,状态设计改为目前成功召唤的鸟数,就是 $ dp_{i,j}$ 表示前\(i\)棵树,召唤了 \(j\) 只鸟剩余最大魔力值,魔力值上限随时计算即可,暴力转移刚好能通过此题
CODE
#include<bits/stdc++.h>
using namespace std;
long long n,w,b,x,c[1010],cost[1010],dp[1010][10100],birds;
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
scanf("%lld%lld%lld%lld",&n,&w,&b,&x);
for(int i=1;i<=n;i++)
scanf("%lld",&c[i]);
for(int i=1;i<=n;i++)
scanf("%lld",&cost[i]);
memset(dp,128,sizeof(dp));
dp[0][0]=w;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=c[i];j++)
for(int k=0;k<=birds;k++)
if(dp[i-1][k]-j*cost[i]>=0)
dp[i][k+j]=max(dp[i][k+j],dp[i-1][k]-j*cost[i]);
birds+=c[i];
for(int k=1;k<=birds;k++)
if(dp[i][k]>=0)
dp[i][k]=min(dp[i][k]+x,w+k*b);
}
for(int i=birds;i>=0;i--)
if(dp[n][i]>=0)
{
printf("%d\n",i);
return 0;
}
return 0;
}
Positions in Permutations
考虑每个位置上能够产生贡献的数,当 \(i=1\) 时只有 \(2\) , \(i=n\) 时只有 \(n-1\) 一个数,其他位置显然有 \(i-1\) 和 \(i+1\) 两个数,其他数我们并不关心
我们发现如果取 \(i-1\) 时,对后面并不会产生影响,而取 \(i+1\) 时会对 \(i+2\) 产生影响,所以我们状压两位来计算贡献 \(\ge i\) 的方案数,钦定必须贡献的位置,对于不需要贡献的位置,随便插一个数就能求出刚才的结果,二项式反演即可,具体看代码注释吧
CODE
#include<bits/stdc++.h>
using namespace std;
const long long mod=1000000007;
long long n,k,dp[1100][1100][5],g[1100],tms[1100],inv[1100],invt[1100],ans,is=1;
long long C(int a,int b){return tms[b]*invt[b-a]%mod*invt[a]%mod;}//计算组合数
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
scanf("%lld%lld",&n,&k);
tms[0]=1;for(int i=1;i<=n;i++) tms[i]=tms[i-1]*i%mod;
inv[1]=1;for(int i=2;i<=n;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
invt[0]=1;for(int i=1;i<=n;i++) invt[i]=invt[i-1]*inv[i]%mod;//预处理阶乘和逆元
dp[1][1][1]=1;
dp[1][0][0]=1;//i=1时仅能选i+1造成贡献
for(int i=2;i<=n;i++)
for(int j=0;j<=n;j++)//j是造成的贡献,填充钦定的位置
{
dp[i][j][0]=(dp[i-1][j][0]+dp[i-1][j][2]+dp[i-1][j-1][0])%mod;
dp[i][j][1]=(dp[i-1][j-1][0]+dp[i-1][j-1][2])%mod;
dp[i][j][2]=(dp[i-1][j][1]+dp[i-1][j][3]+dp[i-1][j-1][1])%mod;
dp[i][j][3]=(dp[i-1][j-1][1]+dp[i-1][j-1][3])%mod;//状压
}//dp值是合法种数
for(int i=0;i<=n;i++)//填充无关紧要的位置
g[i]=(dp[n][i][0]+dp[n][i][2])%mod*tms[n-i]%mod;
for(int i=k;i<=n;i++)//二项式反演
ans=(ans+C(k,i)*g[i]%mod*is+mod)%mod,is=-is;
printf("%lld\n",ans);
return 0;
}
Bear and Cavalry
考虑一个结论:如果战士没有骑马限制,那么将战士和马分别排序后一定是一一对应才会取到最优解,那么如果说有限制呢?
先说结论,我们设一个匹配中几个位置匹配完成后不会和其他位置互相影响的一段为匹配单元,只有以下几种匹配单元是合法的
简单证明一下,考虑\(n=3\)时不在这里的唯一一种情况
此时如果上面的1号点能和下面的1号点匹配,显然有
更优
否则就会被
薄纱
对于\(n>3\)的匹配单元也可以如此证明,归纳即可
所以一个最优的解一定会有若干个如上面重复的匹配单元组成,我们容易想到\(dp\)式子
每次修改暴力 \(O(n) dp\) 卡常即可通过此题,时间复杂度 \(O(nq)\) ,什么?过不了?那就是你的问题
xrlong
的卡常首杀%%%
现在来考虑正解,首先我们在暴力dp的基础上尝试对修改优化,我们发现每次只会改一小段区域,考虑分块,对于每块分别处理,对于块头块尾枚举它交接处匹配单元情况,暴力合并每块即可,每次修改只需 \(O(\sqrt{n})\) 暴力跑出修改的点所在块,接着 \(O(\sqrt{n})\) 合并每块答案即可,总体复杂度 \(O(q\sqrt{n}+n)\),贴个实际表现(我实现常数有点大),后面是代码
CODE
#include<bits/stdc++.h>
using namespace std;
long long n,q,refl[40100],ban[40100],len,is[40100],block[300][3][3],st[300],ed[300],siz,dp[40100],l,r,sum[320][3],to[30010];
pair<long long,long long> a[40100],b[40100];
bool cmp(pair<long,long> u,pair<long,long> v){return u>v;}
void IN()
{
scanf("%lld%lld",&n,&q);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i].first),a[i].second=i;
for(int i=1;i<=n;i++)
scanf("%lld",&b[i].first),b[i].second=i;
sort(b+1,b+1+n,cmp),sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)
refl[b[i].second]=i;
for(int i=1;i<=n;i++)
ban[i]=refl[a[i].second],to[a[i].second]=i;
}
void Divide()
{
len=max(5,(int)pow(n,0.5));
for(int i=1,j=0;i<=n;i++)
{
if(j*len<i)
{
j++;
st[j]=i;
ed[j-1]=i-1;
}
is[i]=j;
}
siz=is[n];
if(siz!=1)
{
for(int i=st[siz];i<=n;i++)
is[i]=siz-1;
siz--;
}
ed[siz]=n;
}
#define cal(q,w) a[q].first*b[w].first
void Solve(int fr,int edd)
{
dp[fr-1]=0;
for(int i=fr;i<=edd;i++)
{
dp[i]=0;
if(ban[i]!=i)
dp[i]=max(dp[i],dp[i-1]+cal(i,i));
if(i-fr+1>1&&ban[i]!=i-1&&ban[i-1]!=i)
dp[i]=max(dp[i],dp[i-2]+cal(i,i-1)+cal(i-1,i));
if(i-fr+1>2&&ban[i]!=i-2&&ban[i-1]!=i&&ban[i-2]!=i-1)
dp[i]=max(dp[i],dp[i-3]+cal(i,i-2)+cal(i-1,i)+cal(i-2,i-1));
if(i-fr+1>2&&ban[i]!=i-1&&ban[i-1]!=i-2&&ban[i-2]!=i)
dp[i]=max(dp[i],dp[i-3]+cal(i,i-1)+cal(i-1,i-2)+cal(i-2,i));
}
}
void pre(int now)
{
Solve(st[now],ed[now]);
block[now][0][0]=dp[ed[now]],block[now][0][1]=dp[ed[now]-1],block[now][0][2]=dp[ed[now]-2];
Solve(st[now]+1,ed[now]);
block[now][1][0]=dp[ed[now]],block[now][1][1]=dp[ed[now]-1],block[now][1][2]=dp[ed[now]-2];
Solve(st[now]+2,ed[now]);
block[now][2][0]=dp[ed[now]],block[now][2][1]=dp[ed[now]-1],block[now][2][2]=dp[ed[now]-2];
}
long long find_sum()
{
memset(sum,0,sizeof(sum));
if(siz==1)
return block[1][0][0];
sum[1][0]=block[1][0][0];
sum[1][1]=block[1][0][1];
sum[1][2]=block[1][0][2];
for(int i=2;i<=siz;i++)
{
for(int k=0;k<=2;k++)
sum[i][k]=max({sum[i-1][0]+block[i][0][k],
sum[i-1][1]+block[i][1][k]+cal(ed[i-1],st[i])+cal(st[i],ed[i-1]),
sum[i-1][1]+block[i][2][k]+cal(ed[i-1],st[i])+cal(st[i],st[i]+1)+cal(st[i]+1,ed[i-1]),
sum[i-1][1]+block[i][2][k]+cal(ed[i-1],st[i]+1)+cal(st[i],ed[i-1])+cal(st[i]+1,st[i]),
sum[i-1][2]+block[i][1][k]+cal(ed[i-1]-1,ed[i-1])+cal(ed[i-1],st[i])+cal(st[i],ed[i-1]-1),
sum[i-1][2]+block[i][1][k]+cal(ed[i-1]-1,st[i])+cal(ed[i-1],ed[i-1]-1)+cal(st[i],ed[i-1])});
}
return sum[siz][0];
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
IN();Divide();
for(int i=1;i<=siz;i++)
pre(i);
for(int i=1;i<=q;i++)
{
scanf("%lld%lld",&l,&r);
swap(ban[to[l]],ban[to[r]]);
pre(is[to[l]]),pre(is[to[r]]);
printf("%lld\n",find_sum());
}
return 0;
}
考虑继续优化,分块 \(O(q\sqrt{n}+n)\) ,那我用线段树维护不就好了,能优化到 \(O(qlogn+n)\)(不过线段树可能还没分块跑的快就是了)
那如果不暴力数据结构维护呢?接下来我们要介绍一种重量级做法 矩阵乘法动态dp
但是这题要取 \(max\) ,我们将矩阵乘法定义中的加号改成 \(max\) 运算,只要证明它满足结合律就好,因为 \(max\) 对加,乘运算都有分配律,所以上述猜想是正确的
在每个位置建立 \(3 \times 3\) 的状态矩阵,线段树维护即可,复杂度 \(O(qlogn+n)\),表现在下面,比分块快一点,空间开销大一些
再次 % xrlong
的正解做法
Future Failure
逆天博弈论+逆天子集卷积,写了,等等补题解
ZS Shuffles Cards
呃呃呃,概率期望
我们发现每次抽卡会抽到三种:
- JOKER牌:我们会进行洗牌,开启下一轮
- 已经抽过的牌:不会对当前局面产生任何影响
- 没抽过的牌:获得一点贡献
所以我们发现每次抽到的牌仅与目前集合内拥有的牌数有关,所以我们设计状态 $ dp_{i} $ 表示集合内已经有了 $ i $ 张牌的期望 轮数 (选择用轮数的原因是将已经抽过的牌的影响降到最低,让后面可做)
这个时候我们只考虑对当前局面产生贡献的两种情况,如果下一张抽到的这两种牌是JOKER,则轮数加一,不造成贡献,而如果是没抽过的,轮数不变,贡献产生一,这剩下一轮里JOKER的数量和有贡献牌的数量和新开一轮是一样的,所以我们有dp方程
边界条件 \(dp_{0}=1\) (我们是用开始的轮数计算的,因为只有抽到一轮结束才查看是否符合结束条件,这也决定了我们可以用轮数dp)
最后暴力跑出每一轮的期望代价 \(k\) , \(k \times dp_{n}\) 即答案
CODE
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long n,m,dp[2000100],inv[4000100],s[2000100],ans=0,sum[2000100];
int main()
{
scanf("%lld%lld",&n,&m);
inv[1]=1;for(int i=2;i<=4000000;i++) inv[i]=(-mod/i+mod)*inv[mod%i]%mod;
dp[0]=1;for(int i=1;i<=n;i++) dp[i]=(dp[i-1]+m*inv[n-i+1]%mod)%mod;
sum[0]=1;for(int i=1;i<=n;i++) sum[i]=sum[i-1]*(n-i+1)%mod*inv[n+m-i+1]%mod;
for(int i=0;i<=n;i++) s[i]=sum[i]*m%mod*inv[n+m-i]%mod;
for(int i=0;i<=n;i++) ans=(ans+s[i]*(i+1))%mod;
printf("%lld",ans*dp[n]%mod);
return 0;
}
Tavas in Kansas
依然博弈论,比某些逆天博弈论题好多了
我们发现,两个人的所有操作只与每个城市到两个人起点的距离有关,而且我们并不关心这个距离的大小,只关心每个城市的先后顺序
我们发现可以把源点到每个城市的距离离散化之后按照顺序放到两个序列里轮流拿取,记录每个人能达到的最优差(即先手的权减去后手的,其中先手期望最大化这个值,后手期望最小化这个值),1表示先手行动,0表示后手行动,两维分别表示两人拿了多少城市,这个显然是要倒着做的,转移方程是
其中函数 \(v1,v2\) 分别表示中间一段城市的权值和,而 \(f\) 表示两个城市序列的后缀重合部分权值
这个式子表面上是 \(O(n^3)\) 的,实际上我们发现 \(dp_{i,j,1}\) 能用到的转移 \(dp_{i+1,j,1}\) 也可以用到(除了它本身),所以用一个数组维护上个决策即可做到 \(O(1)\) 转移(本质上是不会从队首出队的优先队列)
最后检查 \(dp_{0,0,1}\) 正负即可输出答案,复杂度 \(O(n^2)\)
但是这个做法不够优秀,因为它需要大量特判距离相等的情况和权值为零的情况等
将题面转化成二维平面取点问题,一个只能取几行,另一个只能取几列,代码难度大大降低
CODE
#include<bits/stdc++.h>
using namespace std;
long long n,m,s,t,city[2100],mp[2100][2100],a,b,c,dis[2100],sum[2100][2100],x[2100],y[2100],siz1,siz2,cnt[2100][2100],dp[2][2010][2010],p1,p2,f1,f2;
map<long long,long long> m1,m2;
struct NODE
{
long long dis,id;
bool choose;
const friend bool operator < (const NODE & u,const NODE & v){return u.dis<v.dis;}
const friend bool operator == (const NODE & u,const NODE & v){return u.dis==v.dis;}
}sq[2100],tq[2100];
long long find2(int a1,int b1,int a2,int b2){return cnt[a2][b2]-cnt[a1-1][b2]-cnt[a2][b1-1]+cnt[a1-1][b1-1];}
long long find(int a1,int b1,int a2,int b2){return sum[a2][b2]-sum[a1-1][b2]-sum[a2][b1-1]+sum[a1-1][b1-1];}
void dijskra(int fr)
{
bool judge[2100];
long long minx=1e18,minn=0;
memset(judge,0,sizeof(judge));
memset(dis,0x3f,sizeof(dis));
dis[fr]=0;
for(int i=1;i<=n;i++)
{
minx=1e18;
for(int j=1;j<=n;j++)
if(judge[j]==0&&dis[j]<minx)
minx=dis[j],minn=j;
judge[minn]=1;
for(int j=1;j<=n;j++)
if(dis[j]>dis[minn]+mp[minn][j])
dis[j]=dis[minn]+mp[minn][j];
}
}
void prework()
{
dijskra(s);for(int i=1;i<=n;i++) sq[i].dis=dis[i],sq[i].id=i,x[i]=sq[i].dis;
dijskra(t);for(int i=1;i<=n;i++) tq[i].dis=dis[i],tq[i].id=i,y[i]=tq[i].dis;
sort(sq+1,sq+1+n);siz1=unique(sq+1,sq+1+n)-sq-1;
for(int i=1;i<=siz1;i++) m1[sq[i].dis]=i;
sort(tq+1,tq+1+n);siz2=unique(tq+1,tq+1+n)-tq-1;
for(int i=1;i<=siz2;i++) m2[tq[i].dis]=i;
for(int i=1;i<=n;i++)
x[i]=m1[x[i]],
y[i]=m2[y[i]],
sum[x[i]][y[i]]+=city[i],
cnt[x[i]][y[i]]++;
for(int i=1;i<=siz1+1;i++)
for(int j=1;j<=siz2+1;j++)
sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1],
cnt[i][j]+=cnt[i-1][j]+cnt[i][j-1]-cnt[i-1][j-1];
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
for(int i=1;i<=n;i++)
scanf("%lld",&city[i]);
memset(mp,0x3f,sizeof(mp));
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&a,&b,&c);
if(a==b) continue;
mp[a][b]=min(c,mp[a][b]),mp[b][a]=min(c,mp[b][a]);
}
prework();
for(int i=siz1+1;i>=1;i--)
{
for(int j=siz2+1;j>=1;j--)
{
if (i==siz1+1 && j==siz2+1) continue;
if (!find2(i,j,i,siz2)) dp[0][i][j]=dp[0][i+1][j];
else dp[0][i][j]=max(dp[0][i+1][j],dp[1][i+1][j])+find(i,j,i,siz2);
if (!find2(i,j,siz1,j)) dp[1][i][j]=dp[1][i][j+1];
else dp[1][i][j]=min(dp[0][i][j+1],dp[1][i][j+1])-find(i,j,siz1,j);
}
}
if(dp[0][1][1]<0)puts("Cry");
else if(dp[0][1][1]>0)puts("Break a heart");
else if(dp[0][1][1]==0)puts("Flowers");
return 0;
}
Game on Sum (Hard Version)
难得的简单题,考虑 Easy Version 的简单数据,我们发现 \(k\) 的作用很有限,算出 \(k=1\) 时候答案乘上 \(k\) 即可,我们自然想到将Bob剩下的点数和剩下的轮数作为dp状态
既然是博弈论题,尝试倒序考虑,方程比较显然
这个式子显然在 $ dp_{i-1,j-1}-p=dp_{i-1,j}+p $ 时取最小,此时我们有了更好的方程
此时从 \(dp_{0,0}=0\) 开始暴力就可以通过 Easy Version
Hard Version不是我们讨论的重点,将dp值写出来有一个类似杨辉三角的东西简化,组合优化即可
CODE
#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
long long inv2[1000100],inv[1000100],t,n,m,k,invt[1000100],tms[1000100],ans;
long long C(int a,int b){return tms[b]*invt[b-a]%mod*invt[a]%mod;}
int main()
{ms[0]=invt[0]=1;
for(int i=2;i<=1000000;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
inv2[1]=inv[2];for(int i=2;i<=1000000;i++) inv2[i]=inv2[i-1]*inv[2]%mod;
for(int i=1;i<=1000000;i++) tms[i]=tms[i-1]*i%mod,invt[i]=invt[i-1]*inv[i]%mod;
scanf("%lld",&t);
while(t--)
{
scanf("%lld%lld%lld",&n,&m,&k);
ans=0;
if(n==m) ans=n*k%mod;
else if(m==0) ans=0;
else for(int i=m;i>=1;i--) ans=(inv2[n-i]*C(m-i,n-m+m-i-1)%mod*i%mod*k%mod+ans)%mod;
printf("%lld\n",ans);
}
return 0;
}