概率和期望

定义

概率,即概率,不提。

期望如下定义:

对于事件 \(P\) 可能产生的 \(n\) 个贡献为 \(val_i\) 的结果,每个结果概率为 \(p_i\),则称事件 \(P\) 的数学期望:

\[E(P)=\sum_{i=1}^n{val_ip_i} \]

翻译成人话就是事件结果反映权值的平均数。

期望线性性?

不同事件间的期望得以累加,形式化地,\(\forall A,B,E(A+B)=E(A)+E(B)\)

左边的加指的是事件同时成立。

举个例子。事件 \(A\) 为wmw在一段时间 \(n\) 内每天等概率地与wymx增加 \(val_i\) 点亲密度。

\[E(A)=\frac{1}{n}\sum^n val_i \]

事件 \(B\) 为cjx在这段时间内每天也以 \(p_i\) 概率与nw增加 \(val_i\) 点亲密度。

\[E(B)=\sum^n{val_ip_i} \]

由此可以得到:cjx泡妹子看脸两个人每天为人类延续的伟大事业做出的贡献期望 \(E(A+B)\) 为:

\[\sum^n val_i(p_i+\frac{1}{n}) \]

这意味着:处理整个事件的期望时,可以将该事件的子事件按拓扑线性转移。事件的拓扑一般也是线性的。因而导致概率与期望的最好处理方法是 dp。

OSU1

给定打一把音游的结果,计算成绩。只有 P 和 Miss,还有不知道结果的 note,判定对半开,存在 combo 机制为 \(combo(x)=x^2\)。求这把的期望得分。

这人准度咋这么低啊

思考:如何处理连击分?

如果现在已经有一段长 \(x\) 的combo 给出了 \(x^2\) 的贡献,则下一个 \(?\) 也为 P 时,该 note 做出的分数贡献为:\((x+1)^2-x^2=2x+1\)

我们发现不仅要计算期望得分,还得计算期望 combo。

\(f_i\) 表示到第 \(i\) 个 note 时的期望得分,\(g_i\) 表示到 \(i\) 个note 时的期望combo。

\(str_i=x:\)此时不得分,且会断连。

\[f[i]=f[i-1],g[i]=0 \]

\(str_i=o\):此时一定按 combo 得分,对combo 做出概率为 1 的贡献。

\[f[i]=f[i-1]+2g[i-1]+1,g[i]=g[i-1]+1 \]

\(str_i=?\):此时有一半概率不得分,按照期望公式:

\[f[i]=f[i-1]+\frac{1}{2}(2g[i-1]+1) \]

\[ g[i]=\frac{1}{2}(g[i-1]+1) \]

注意不得分时是断连,所以 \(g[i-1]\) 也要加入 \(\frac{1}{2}\) 中。

#include<bits/stdc++.h>
#define MAXN 300005
using namespace std;
int n;
char str[MAXN];
double f[MAXN],g[MAXN];
int main(){
	scanf("%d",&n);
	scanf("%s",str+1);
	for(int i=1;i<=n;i++){
		if(str[i]=='x')f[i]=f[i-1],g[i]=0;
		if(str[i]=='o')f[i]=f[i-1]+2*g[i-1]+1,g[i]=g[i-1]+1;
		if(str[i]=='?')f[i]=f[i-1]+1.0*(2*g[i-1]+1)/2,g[i]=1.0*(g[i-1]+1)/2;
	}
	printf("%.4f",f[n]);
	return 0;
}

OSU2

byd这把准度完全看命了。连击分变成了 \(x^3\) 这意味着新的一次打击能做出 \((x+1)^3-x^3=3x^2+3x+1\) 的贡献。

同时 \(x^2\) 也会增加 \(2x+1\),这是上一道题的结论了。

所以开三个 dp 数组维护即可。

#include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
int n;
double dp[5][MAXN];
double v[MAXN];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lf",&v[i]);
	for(int i=1;i<=n;i++){
		dp[1][i]=(1+dp[1][i-1])*v[i];
		dp[2][i]=(dp[2][i-1]+2*dp[1][i-1]+1)*v[i];
		dp[3][i]=dp[3][i-1]+(3*dp[2][i-1]+3*dp[1][i-1]+1)*v[i];
	}
	printf("%.1f",dp[3][n]);
	return 0;
}

绿豆蛙的归宿

此时事件的拓扑不是线性的了,不过是 DAG,所以直接跑一遍拓扑排序。

注意此题中一个点 \(v\) 能够对期望做出的贡献为:到达连接该点的概率 \(p_u\) 与边权 \(w\) 之积。

\[dp_v=\frac{1}{siz_u}\sum_{(u,v)\in E}dp_u+p_uw_{(u,v)} \]

所以如果顺推得先处理一遍概率。

#include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
int n,m;
struct node{
	int v;
	double w;
};
vector<node>edge[MAXN];
int siz[MAXN],ind[MAXN],indd[MAXN];
double p[MAXN],dp[MAXN];
queue<int>Q;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,u,v,w;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		edge[u].push_back({v,w});
		++siz[u],++indd[v],ind[v]=indd[v];
	}
	p[1]=1.0;
	Q.push(1);
	while(!Q.empty()){
		int u=Q.front();
		Q.pop();
		for(int i=0;i<edge[u].size();i++){
			int v=edge[u][i].v;
			double w=edge[u][i].w;
			--indd[v];
			p[v]+=p[u]/siz[u];
			if(!indd[v])Q.push(v);
		}
	}
	Q.push(1);
	while(!Q.empty()){
		int u=Q.front();
		Q.pop();
		for(int i=0;i<edge[u].size();i++){
			int v=edge[u][i].v;
			double w=edge[u][i].w;
			--ind[v];
			dp[v]+=(dp[u]+p[u]*w)/siz[u];
			if(!ind[v])Q.push(v);
		}
	}
	printf("%.2f",dp[n]);
	return 0;
}

列队春游

乍一看很不好做。实则确实不好做

考虑从单个人入手,将人按身高排序作 \(p_1...p_n\),假设人的身高是不重复的,则 \(p_k\) 的视距 \(val_k\),满足 \(0\le val_k\le k\)。重复时:\(0\le val_k \le k+siz_k-1\),其中 \(siz_k\) 指升高与 \(p_k\) 相同的个数。

为了处理方便,令 \(F(k)=val_k+siz_k-1\)

于是单个人的贡献为:

\[E(k)=\sum_{i=1}^{F(k)}i*P \]

即前 \(i\) 个人都不高于他的概率。

然后考虑计算这个概率,显然是一个排列组合问题。

假设有 \(G(k)\) 个不包括他的人身高不低于他,用总方案数除以可行方案即概率 \(P\)

\[G(k)=n-rk_k+1,\forall k\in[1,n],p_k\le p_{k+1},p_{rk_k}=p_{rk_k+1}=...=p_{rk_k+siz_k-1} \]

\[E(k)=\sum_{i=1}^{n}\frac{(n-i+1)A_{n-i}^{G(k)}}{A_{n}^{G(k)+1}} \]

\[\sum_{i=1}^{n}\frac{(n-i+1)A_{n-i}^{G(k)}}{A_{n}^{G(k)+1}} \]

\[=\sum_{i=1}^{n}\frac{(n-i+1)(n-i-G(k)+1)(n-i-G(k)+2)...(n-i)}{(n-G(k))(n-G(k)+1)...n} \]

\[=\sum_{i=1}^{n}\frac{(n-G(k)-1)!(n-i-1)!}{n!(n-i-G(k))!} \]

\[=\frac{(n-G(k)-1)!}{n!}\sum_{i=1}^{n}\frac{(n-i-1)!}{(n-i-G(k))!} \]

\[=\frac{(n-G(k)-1)!}{n!}\sum_{i=1}^{n}(G(k)+1)!C_{n-i+1}^{G(k)+1} \]

\[\frac{(n-G(k)-1)!(G(k)+1)!}{n!}\sum_{i=1}^{n}C_{n-i+1}^{G(k)+1} \]

然后发现化不动了。

想到:\(C_{n}^{m}=C_{n-1}^{m}+C_{n-1}^{m-1}\)

后面的部分展开:

\[C_{n}^{G(k)+1}+C_{n-1}^{G(k)+1}...+C_{1}^{G(k)+1} \]

\[=C_{n}^{G(k)+1}+...C_1^{G(k)+1}+C_1^{G(k)+2}\ (C_1^{G(k)+2}=0) \]

\[=C_{n}^{G(k)+1}+C_{n-1}^{G(k)+1}+...C_2^{G(k)+1}+C_2^{G(k)+2} \]

\[=C_{n}^{G(k)+1}+C_{n}^{G(k)+2}=C_{n+1}^{G(k)+2} \]

于是原式化为:

\[E(k)=C_{n+1}^{G(k)+2}\frac{(n-G(k)-1)!(G(k)+1)!}{n!} \]

\[=\frac{(n+1)!(n-G(k)-1)!(G(k)+1)!}{(G(k)+2)!(n-G(k)-1)!n!} \]

\[=\frac{n+1}{G(k)+2} \]

非常简洁。

\[E=\sum_{i=1}^n E(i)=\sum_{i=1}^n \frac{n+1}{G(i)+2} \]

\(G(i)\) 函数乱搞即可,不放码了。

守卫者的挑战

\(dp[i][j][k]\) 表示到第 \(i\) 次挑战成功了 \(j\) 次,剩余背包容量为 \(k\) 时的概率。

这个题很坑,背包容量不足了其实可以先拿着,只要最后的容量大于等于零即可。

因而当前容量可以为负数,所以把容量挪一下,又注意到最多只会消耗 200 的容量,即 \(n\),所以容量超过 \(n\) 的当 \(n\) 处理。

\[ans=\sum_{i>=l,j>=n}^{i<=n,j<=2n}dp[n][i][j] \]

考虑转移,失败时:

\[dp[i][j][k]=dp[i-1][j][k] \]

成功时:

\[dp[i][j][k]=dp[i-1][j-1][k-val[i]] \]

两者的概率分别为 \(1-p_i,p_i\)

#include<bits/stdc++.h>
#define MAXN 405
using namespace std;
int n,l,k;
double dp[2][205][MAXN];
int val[MAXN];
double p[MAXN],ans;
int main(){
	scanf("%d%d%d",&n,&l,&k);
	k=min(n,k);
	for(int i=1;i<=n;i++)scanf("%lf",&p[i]),p[i]/=100.0;
	for(int i=1;i<=n;i++)scanf("%d",&val[i]);
	dp[0][0][n+k]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=n;j++)
			for(int k=0;k<=n*2;k++)dp[i%2][j][k]=dp[(i+1)%2][j][k]*(1-p[i]);
		for(int j=0;j<n;j++)
			for(int v=1;v<=n*2;v++){
				int siz=min(v+val[i],2*n);
				dp[i%2][j+1][siz]+=dp[(i+1)%2][j][v]*p[i];
			}	
	}
	for(int i=l;i<=n;i++)for(int j=0;j<=n;j++)ans+=dp[n%2][i][j+n];
	printf("%.6f",ans);
	return 0;
}

这个题会卡空间,注意到当前的 \(i\) 只会从 \(i-1\) 转移,第一维用滚动数组滚到 2 容量即可。

卡牌游戏

剩下一个人时该人胜率为 \(100\%\),所以应该从这个状态逐步转移到剩下 \(n\) 个人时第 \(i\) 个人的胜率 \(dp[n][i]\)

由于游戏每轮固定杀掉一个人,所以第 \(i\) 轮的序列有 \(i\) 人,每轮有 \(\frac{1}{m}\) 的概率抽到一张牌并生成新的序列,也就是说每个状态的转移概率是 \(\frac{1}{m}\)

\(\frac{1}{m}\) 的概率抽到第 \(j\) 张牌时,从庄家开始的第 \(c_j\%i\) 人将被杀掉,我们计算每个人在一个人被杀掉后的新位置。

又令 \(dp[i][j]\) 表示第 \(i\) 轮从庄家开始的第 \(j\) 个人获胜的概率,这样同时迎合了转移和答案的格式。

对于第\(i\) 轮从庄家开始的 \(j\) 个人,在抽到第 \(k\) 张牌时:

\[loc=c_k\%i \]

\[dp[i][n-loc+j]=\frac{1}{m}\sum dp[i-1][j],loc>j \]

\[dp[i][j-loc]=\frac{1}{m}\sum dp[i-1][j],loc<j \]

照着打就能过。

#include<bits/stdc++.h>
#define MAXN 55
using namespace std;
int n,m;
double dp[MAXN][MAXN];
int c[MAXN];
int main(){
   scanf("%d%d",&n,&m);
   for(int i=1;i<=m;i++)scanf("%d",&c[i]);
   dp[1][1]=1;
   for(int i=2;i<=n;i++){
   	for(int j=1;j<=i;j++){
   		for(int k=1;k<=m;k++){
   			int loc=c[k]%i;
   			if(!loc)loc=i;
   			if(loc>j)dp[i][j]+=dp[i-1][i-loc+j]/m;
   			if(loc<j)dp[i][j]+=dp[i-1][j-loc]/m;
   		}
   	}
   }
   for(int i=1;i<=n;i++)printf("%.2f",dp[n][i]*100),putchar('%'),putchar(' ');
   return 0;
}

换教室

一道很烦的题,到处都会挂分。

由于不同概率导致的结果会导致路径权值不同,我们使用 \(dp[i][j][0/1]\) 表示时间为 \(i\) 时申请换了 \(j\) 次课,当前时间不换/换 的期望最短路。

如果这次不换:

上次也可能没换,或者换了,然换了要考虑换没换成。

\[dp[i][j][0]=min\begin{cases} dp[i-1][j][0]+dis(c[i],c[i-1]) \\ dp[i-1][j][1]+p_{i-1}dis(d[i-1],c[i])+(1-p_{i-1})dis(c[i-1],c[i]) \end{cases} \]

如果这次换了:

那也要同时考虑这次换没换成。

\[dp[i][j][1]=min\begin{cases} dp[i-1][j-1][0]+p_idis(d[i],c[i-1])+(1-p_i)dis(c[i],c[i-1]) \\ dp[i-1][j-1][1]+p_ip_{i-1}dis(d[i],d[i-1])+(1-p_i)p_{i-1}dis(c[i],d[i-1])+p_i(1-p_{i-1})dis(d[i],c[i-1])+(1-p_i)(1-p_{i-1})dis(c[i-1],c[i]) \end{cases} \]

\(v\) 很小,使用 Floyd。

注意:

  • \(dis_{i,j}\) 是浮点类型。
  • 浮点数不可使用 memset()。
  • 有重边自环。
  • Floyd 的枚举顺序为 \(k,i,j\),这个很重要。
  • \(j=0\) 时无法转移 \(dp[i][j][1]\)
#include<bits/stdc++.h>
#define MAXN 2005
#define MAXM 305
#define int long long
using namespace std;
int n,m,V,e;
double dis[MAXM][MAXM];
int c[MAXN],d[MAXN];
double p[MAXN];
double dp[MAXN][MAXN][2];
double ans=1e9;
signed main(){
	scanf("%lld%lld%lld%lld",&n,&m,&V,&e);
	for(int i=0;i<=V;i++)for(int j=0;j<=V;j++)dis[i][j]=1e9;
	for(int i=1;i<=n;i++)scanf("%lld",&c[i]);
	for(int i=1;i<=n;i++)scanf("%lld",&d[i]);
	for(int i=1;i<=n;i++)scanf("%lf",&p[i]);
	for(int i=1;i<=V;i++)dis[i][i]=dis[i][0]=dis[0][i]=0;
	for(int i=1,u,v,w;i<=e;i++){
		scanf("%lld%lld%lld",&u,&v,&w);
		dis[u][v]=min(dis[u][v],1.0*w);
		dis[v][u]=min(dis[v][u],1.0*w);
	}
	for(int k=1;k<=V;k++)
		for(int i=1;i<=V;i++)
			for(int j=1;j<=V;j++){
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
				dis[j][i]=min(dis[j][i],dis[i][k]+dis[k][j]);
			}
	for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)dp[i][j][0]=dp[i][j][1]=1e9;
	dp[1][1][1]=dp[1][0][0]=0;
	for(int i=2;i<=n;i++){
		dp[i][0][0]=dp[i-1][0][0]+dis[c[i]][c[i-1]];
		for(int j=1;j<=min(i,m);j++){
			dp[i][j][0]=dp[i-1][j][0]+dis[c[i]][c[i-1]];
			dp[i][j][0]=min(dp[i][j][0],dp[i-1][j][1]+p[i-1]*dis[c[i]][d[i-1]]+(1.0-p[i-1])*dis[c[i]][c[i-1]]);
		}
		for(int j=1;j<=min(i,m);j++){
			dp[i][j][1]=dp[i-1][j-1][0]+p[i]*dis[d[i]][c[i-1]]+(1.0-p[i])*dis[c[i]][c[i-1]];
			dp[i][j][1]=min(dp[i][j][1],dp[i-1][j-1][1]+p[i-1]*p[i]*dis[d[i]][d[i-1]]+p[i-1]*(1.0-p[i])*dis[c[i]][d[i-1]]+(1.0-p[i-1])*p[i]*dis[c[i-1]][d[i]]+(1.0-p[i])*(1.0-p[i-1])*dis[c[i]][c[i-1]]);
		}
	}
	for(int i=0;i<=m;i++)ans=min(ans,min(dp[n][i][0],dp[n][i][1]));
	printf("%.2f",ans);
	return 0;
}

概率充电器

自己想的最多的一道题,虽然最后也颓了下题解。

首先:期望的树上问题大概率用 树形dp 解决,如果去考虑一遍 dfs 预处理后按点间关系实现线性转移大概率要爆。

根据公式:

\[ans=\sum_{i=1}^nE(i)=\sum_{i=1}^np_iw_i \]

\[\forall i,w_i=1\Rightarrow ans=\sum_{i=1}^np_i \]

也就是计算每个点被点亮的概率和。

一个点可以这样点亮:

  • 自己以 \(p_i\) 的概率亮了
  • 被父亲节点以 \(P(fa)\) 的概率传导点亮了
  • 被子节点以 \(P(son)\) 的概率传导点亮了

第一条可以初始化时就算好,即

\[dp_i=q_i \]

\(P(i)\) 函数显然是要通过给出的概率计算的,因为 \(P(i)\) 也包含点 \(i\) 自己点亮与被传导点亮的概率。

而对于两个点 \(i,j\)\(i\) 自己点亮后会对 \(j\) 产生贡献得到 \(dp_j\),而 \(i\) 在不亮时被传导点亮的概率显然不是 \(dp_j\)。因为这其中包括了:“\(i\) 点亮而 \(j\) 不亮时 \(i\) 点亮 \(j\)”这一情况的概率。

我们显然不能高效地在 树形dp 中处理这种混乱的关系。而这种关系是由于想要同时考虑 \(P(fa)\)\(P(son)\) 导致的。

这启示我们分开来看。从任一根节点开始计算,发现\(P(fa)\) 会受到除了 \(i\) 以外的节点 \(P(son_{fa})\) 影响,所以要先处理 \(P(son)\)

对于节点 \(i\) 与他的子节点 \(j\in son_i\),想当然地想到

\[dp_i=dp_i+dp_j*P(link) \]

不过这个显然是错的,给定 \(q_i=q_j=50\%,P(link(i,j))=100\%\),我们发现 \(j\) 成了必然点亮的东西。

不妨思考 \(i\) 被点亮的概率 \(P(i)\)\(j\) 被点亮的概率 \(P(j)\) 间的转移,只有 \(i\) 自己不亮,\(j\) 也没有传过来时 \(i\) 才不亮。

\[P(i)=1-((1-dp_i)*(1-P(link)*P(j))) \]

\[=dp_i+P(link)P(j)-dp_iP(j)P(link) \]

每个子节点 \(j\) 都可以在一遍 \(dfs()\) 中完成转移。

现在单个考虑第三条如何成立。

\(j\) 能被父亲 \(i\) 点亮时,\(P(i)\) 应该减去 \(j\) 对其做出的贡献,不过显然是不能直接减的,我们考虑刚才的式子:

\[P(i)=dp_i+P(link)P(j)-dp_iP(j)P(link) \]

此时 \(dp_i\) 才是父节点 \(i\)\(j\) 断电时能传导给其的概率。还好它是可以反推的。

\[dp_i-dp_iP(j)P(link)=P(i)-P(link)P(j) \]

\[dp_i=\frac{P(i)-P(link)P(j)}{1-P(j)P(link)} \]

此时同理得到 \(j\) 被父节点传递的概率为:

\[P(j)=P(link)dp_i+dp_j-P(link)dp_idp_j \]

如此,使用两遍 dfs 后汇总即可。

但是注意 \(P(j)P(link)=1\) 时就别转了,会爆。这里使用
愤怒的小鸟 里的 \(fabs()\) 技巧处理。

#include<bits/stdc++.h>
#define MAXN 500005
using namespace std;
int n;
struct node{
	int v;
	double p;
};
vector<node>edge[MAXN];
double q[MAXN];
double dp[MAXN],ans;
inline void dfs2(int u,int fa){
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i].v;
		double w=edge[u][i].p;
		if(v==fa)continue;
		dfs2(v,u);
		dp[u]=(dp[u]+w*dp[v]-dp[u]*dp[v]*w);
	}
}
inline bool check(double val){
	return (fabs(val-1.0)<=1e-7);
}
inline void dfs(int u,int fa){
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i].v;
		double w=edge[u][i].p;
		if(v==fa)continue;
		if(!check(dp[v]*w)){
			double falink=(dp[u]-w*dp[v])/(1.0-dp[v]*w);
			dp[v]=(dp[v]+w*falink-dp[v]*falink*w);
		}
		dfs(v,u);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1,u,v;i<n;i++){
		double p;
		scanf("%d%d%lf",&u,&v,&p);
		p/=100.0;
		edge[u].push_back({v,p});
		edge[v].push_back({u,p});
	}
	for(int i=1;i<=n;i++)scanf("%lf",&q[i]),q[i]/=100.0;
	for(int i=1;i<=n;i++)dp[i]=q[i];
	dfs2(1,0);
	dfs(1,0);
	for(int i=1;i<=n;i++)ans+=dp[i];
	printf("%.6f",ans);
	return 0;
}

分手是祝愿

题太吊了。

看了快一个小时一点思路没有,所以直接说题解做法。

首先,对于这样的一个 01串,存在唯一解法使得串能被归零。

解为:从右往左依次扫描,发现开着的灯就关掉。

证明:不难想这一定是可行解,现在要证这样的解法是唯一解。

对于任意一盏灯,其开关次数都不会超过一次,因为这样的操作是取异或,开关两次相当于没动,三次相当于一次。

不妨把一种解法对开关灯的编号汇总为集合 \(S_i\),易知一种解法 \(S_k\) 对状态 \(C\) 是对应的,要证明是唯一解,只需证 映射 \(f:S\to C\) 为单射。

\(\because |C|=|S|=2^n\),只需证 \(f\) 为满射。

反证,不妨设存在状态 \(C_0\) 使得 \(\forall S_i,f:S_i\nrightarrow C_0\),然无论如何,上文解法一定可以将 \(C_0\) 变为全 0 的串,矛盾。

因此,任何情况都有唯一关灯方法即上文提到的方法。

这个结论有什么用呢?这告诉我们,在 \(n\) 盏灯中,当且仅当某 \(val\) 盏灯都被关闭时,初始状态可被清零。

也就是说在无穷的随机开关灯中,B 君只有 \(val\) 次操作是正确的,且每多一次无用操作,都需要一次额外的操作来偿还这次失误。

这不就成概率期望题了?

初始化后,令 \(dp_i\) 表示还剩 \(i(i\le val)\) 盏必须关闭的灯时,把 \(i\) 关到 \(i-1\) 时的期望操作。

则当前情况下:有 \(\frac{i}{n}\) 的概率是有效操作,而有 \(\frac{n-i}{n}\) 的操作是无效操作,需要偿还。且此时必须要关掉的灯变成了 \(i+1\) 盏,还需要期望为 \(dp_{i+1}\) 次操作才能复原。复原后,又需要期望为 \(dp_i\) 次操作才能变到 \(i-1\)

\[dp_i=\frac{i}{n}+\frac{n-i}{n}(dp_{i+1}+dp_i+1) \]

整理一下:

\[dp_i+\frac{i-n}{n}dp_i=\frac{i+(n-i)(dp_{i+1}+1)}{n} \]

\[\frac{i}{n}dp_i=\frac{(n-i)dp_{i+1}+n}{n} \]

\[dp_i=\frac{(n-i)dp_{i+1}+n}{i},dp_{n+1}=0 \]

我们发现完成归零时的期望转移和状态完全无关了,完美解决了难以设计状态的问题。

又因为剩下 \(k\) 次时只需要 \(k\) 次操作就可以归零。

\[ans=\begin{cases} k,val<k \\ k+\sum_{i=k+1} ^{n}dp_i \end{cases} \]

然后就没了。

#include<bits/stdc++.h>
#define MAXN 100005
#define int long long
using namespace std;
const int p=100003;
int f[MAXN];
inline void INIT(){
	f[0]=1;
	for(int i=1;i<MAXN;i++)f[i]=f[i-1]*i%p;
}
int n,k;
int l[MAXN],val,ans;
int dp[MAXN];
inline int qpow(int base,int power){
	int res=1;
	while(power){
		if(power&1)res=res*base%p;
		base=base*base%p;
		power>>=1;
	}
	return res%p;
}
signed main(){
	INIT();
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++)scanf("%lld",&l[i]);
	for(int i=n;i>=1;i--){
		if(l[i]){
			++val;
			for(int j=1;j*j<=i;j++){
				if(i%j==0){
					l[j]^=1;
					if(j*j!=i)l[i/j]^=1;
				}
			}
		}
	}
	for(int i=n,tmp=0;i>=1;i--){
		tmp=((n-i)*dp[i+1]%p+n)%p;
		tmp=tmp*qpow(i,p-2)%p;
		dp[i]=tmp;
	}
	if(val<=k)ans=val;
	else{
		ans=k;
		for(int i=val;i>k;i--)ans=(ans+dp[i])%p;
	}
	printf("%lld",ans*f[n]%p);
	return 0;
}

还有几道难题需要高斯消元,不过我现在不会,此帖暂时完结。

posted @ 2024-03-16 17:14  RVm1eL_o6II  阅读(9)  评论(0编辑  收藏  举报