杂题之dp(1-10)
\(1\):\(UVA1437\)
https://www.luogu.com.cn/problem/UVA1437
题解:最优解下,任意两次覆盖区间\([l,r]\),一定是嵌套或并列的。
考虑\(dp\),分类处理嵌套和并列两种情况。
先处理嵌套,发现经过任意一次覆盖后,原本的\(A\)就无意义了。
于是设\(g(k,i,j)\)为当\(i\sim j\)全为字符\(k\)时,变成\(B_i\sim B_j\)的最小花费。
若\(B_i=B_j\),由于当前仅考虑嵌套关系,所以若\(B_i=B_j\),则一定可以先覆盖\([i,j]\)再考虑\([i+1,j]\)或\([i,j-1]\)。
转移方程为\(g(k,i,j)=min\{g(k,i+1,j),g(k,i,j-1)\}+(k\neq B_i)\)。
若\(B_i\neq B_j\),则两端点一定是要被不同区间覆盖,所以枚举断点。
转移方程为\(g(k,i,j)=g(B_i,i,mid)+(B_i\neq k)+g(B_j,mid+1,j)+(B_j\neq k)\)
对于并列关系,令\(f(i)\)为完成\(i\)个字符的最小代价。
若\(A_i=B_i\),则将\(f(i-1)\)转移至\(f(i)\)。
其余情况,考虑枚举最后一个嵌套结构大小。
即\(f(i)=min\{f(j)+g(B_i,j+1,i)\},0\leq j\leq i-1\)。
#include <bits/stdc++.h>
using namespace std;
const int N=110;
const int Base=131,P=998244353;
int n;
int f[N],g[30][N][N];
char A[N],B[N];
int main(){
while(cin >> A+1 >> B+1){
n=strlen(A+1);
memset(f,0,sizeof f);
memset(g,0,sizeof g);
for(int len=1; len<=n; len++)
for(int l=1; l+len-1<=n; l++){
int r=l+len-1;
for(char ch='a'; ch<='z'; ch++){
for(int k=l; k<=r; k++)
if(ch!=B[k]) g[ch-'a'][l][r]++;
if(B[l]==B[r]) g[ch-'a'][l][r]=min(g[ch-'a'][l][r],
min(g[ch-'a'][l][r-1],g[ch-'a'][l+1][r])+(ch!=B[l]));
for(int k=l; k<=r-1; k++)
g[ch-'a'][l][r]=min(g[ch-'a'][l][r],g[B[l]-'a'][l][k]+
(ch!=B[l])+g[B[r]-'a'][k+1][r]+(ch!=B[r]));
}
}
for(int i=1; i<=n; i++){
for(int j=1; j<=i; j++)
if(A[j]!=B[j]) f[i]++;
if(A[i]==B[i]) f[i]=f[i-1];
bool val=0;
for(int j=i-1; j>=0; j--){
if(A[j+1]!=B[i]) val=1;
f[i]=min(f[i],f[j]+g[B[i]-'a'][j+1][i]+val);
}
}
cout << f[n] << endl;
}
return 0;
}
\(2\):\(P3239\)
https://www.luogu.com.cn/problem/P3239
题解:拆贡献,考虑每张卡牌被使用的概率\(f_i\),答案即为\(\sum f_i*d_i\)。
显然有\(f_1=1-(1-p_1)^r\),这是因为针对第\(i\)张牌随机了\(r\)次。
受到启发,考虑枚举第\(i\)张牌随机次数。
则有\(f_i=\sum u(i,j)*(1-(1-p_i)^j)\),\(u(i,j)\)为\(r\)轮里,随机第\(i\)张牌\(j\)次概率。
第\(i\)张牌随机\(j\)次,等价于有\(r-j\)轮在前\(i-1\)张牌内结束。
于是设\(g(i,j)\)为\(r\)轮里,前\(i\)张牌选了\(j\)张概率。
则有\(f_i=\sum g(i-1,j)*(1-(1-p_i)^{r-j})\)。
现在只需求出\(g\)数组即可,针对选不选第\(i\)张牌分类。
若不选第\(i\)张牌,则\(g(i,j)\leftarrow g(i-1,j)*(1-p_i)^{r-j}\)。
若选第\(i\)张牌,则\(g(i,j)\leftarrow g(i-1,j-1)*(1-(1-p_i)^{r-j+1})\)。
#include <bits/stdc++.h>
using namespace std;
const int N=510;
int n,r;
int d[N];
double p[N],f[N],g[N][N];
double qpow(double a,int b){
double res=1;
while(b){
if(b&1) res*=a;
a*=a;
b>>=1;
}
return res;
}
int main(){
int T;
cin >> T;
while(T--){
cin >> n >> r;
for(int i=1; i<=n; i++) cin >> p[i] >> d[i];
memset(g,0,sizeof g);
g[0][0]=1;
for(int i=1; i<=n; i++)
for(int j=0; j<=i; j++){
if(r-j>=1) g[i][j]+=g[i-1][j]*qpow(1-p[i],r-j);
if(j&&r-j+1>=1) g[i][j]+=g[i-1][j-1]*(1-qpow(1-p[i],r-j+1));
}
double ans=0;
for(int i=1; i<=n; i++)
for(int j=0; j<=min(i,r); j++)
ans+=d[i]*g[i-1][j]*(1-qpow(1-p[i],r-j));
printf("%.10lf\n",ans);
}
return 0;
}
\(3\):\(P2900\)
https://www.luogu.com.cn/problem/P2900
若对于矩形\(i\),存在\(j\),使得\(w_i\leq w_j\),且\(l_i\leq l_j\),则矩形\(i\)一定可包含在矩形\(j\)内,不对答案产生影响。
按\(w\)为第一关键字,\(l\)为第二关键字从小到大排序,然后关于\(l\)进行单调栈,即可去除无用矩阵。
剩下的矩阵一定是\(w\)递增,\(l\)递减的,此时最优策略一定是若干相邻矩阵打包。
于是考虑\(dp\),设\(f_i\)为前考虑前\(i\)个矩阵的最小花费。
有转移\(f_i=min\{f_j+w_i*l_{j+1}\}\)。
可看成有若干条\(a_i=l_{i+1},b_i=f_i\)的直线\(y=a_i*x+b_i\),每次答案即为令\(x=w_i\)时,最小的\(y\)。
此题有良好的性质,即截距递增,斜率递减。
用单调队列维护直线,相邻两直线可确定一交点,当\(x\)小于交点时,前直线优,否则后直线优。
于是每次当队首两直线交点小于等于当前\(w_i\)时,将队首直线出队。
同时用常规方法维护直线集合的上凸壳,即若队尾两条直线为\(l_1,l_2\),新加入直线\(l_3\)。
若\(l_1,l_2\)交点横坐标大于\(l_2,l_3\)交点横坐标,将\(l_2\)出队,直至满足条件。
同时用单调性维护队首队尾,即可保证队首即为最优解。
#include <bits/stdc++.h>
#define int long long
#define w first
#define l second
using namespace std;
typedef pair<int,int> PII;
const int N=5e4+10;
int n;
int f[N];
int stk[N],top;
int q[N],hh,tt=-1;
PII a[N];
double get(int i,int j){
int a1=a[i+1].l,b1=f[i];
int a2=a[j+1].l,b2=f[j];
return (double)(b2-b1)/(a1-a2);
}
signed main(){
cin >> n;
for(int i=1; i<=n; i++){
int w,l;
cin >> w >> l;
a[i]={w,l};
}
sort(a+1,a+n+1);
for(int i=1; i<=n; i++){
while(top&&a[i].l>=a[stk[top]].l) top--;
stk[++top]=i;
}
n=top;
for(int i=1; i<=n; i++) a[i]=a[stk[i]];
for(int i=1; i<=n; i++){
while(hh+1<=tt&&get(i-1,q[tt])<=get(q[tt],q[tt-1])) tt--;
q[++tt]=i-1;
while(hh+1<=tt&&get(q[hh],q[hh+1])<=a[i].w) hh++;
f[i]=a[q[hh]+1].l*a[i].w+f[q[hh]];
}
cout << f[n] << endl;
return 0;
}
\(4\):\(P8476\)
https://www.luogu.com.cn/problem/P8476
题解:考虑贪心,对于任意\(b_i\),一定存在一\(a_j=b_i\)。
于是令\(f(i,j)\)为\(b_i\)匹配\(a\)中第\(j\)大数的最优方案。
有转移式,\(f(i,j)=min\{f(i-1,k)\}+val(i,j)\),\(k\leq j\)。
若令\(g(i,j)=min{f(i-1,k)}\),\(k\leq j\)。
则\(f(i,j)=g(i-1,j)+val(i,j)\)。
设\(a_i\)是\(a\)中第\(k\)大,则若\(j\leq k\),\(val(i,j)=c_j-a_i\),若\(j>k\),\(val(i,j)=C\)。
考虑\(g(i,j)\leq g(i,j-1)\),且当\(j\neq k\)时,\(val(i,j)\leq g(i,j-1)\)。
所以当\(j\neq k\)时,\(f(i,j)\leq f(i,j-1)\),也代表着\(g\)序列是由两个单调不增的序列首位拼接而成。
所以可以先每个点用\(f\)代替\(g\),然后用\(g(i,k)\),对\([k+1,n]\)区间取\(min\)。
取\(min\)操作维护较复杂,可考虑在第二个序列上二分出最后一个大于\(g(i,k)\)的位置,然后对这部分区间覆盖。
对于\(+C,-a_i\)直接用区间加减操作即可。
问题是对于\(+b_j\)是区间加一个序列,如何在线段树上维护呢。
\(g\)和\(b\)是两个单调不增序列,他们和的最小值,仍然是右端点,所以用懒标记维护序列叠加次数,计算时直接用右端点计算最小值即可。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10;
int n,C;
int a[N],b[N];
struct SegmentTree{
struct Node{
int l,r;
int mi,mx,cov;
int add1,add2;
}tr[4*N];
void pushup(int u){
tr[u].mi=min(tr[u<<1].mi,tr[u<<1|1].mi);
tr[u].mx=max(tr[u<<1].mx,tr[u<<1|1].mx);
}
void change(int u,int add1,int add2,int cov){
if(cov!=-1){
tr[u].mi=tr[u].mx=tr[u].cov=cov;
tr[u].add1=tr[u].add2=0;
}
tr[u].mi+=add1+b[tr[u].r]*add2;
tr[u].mx+=add1+b[tr[u].l]*add2;
tr[u].add1+=add1,tr[u].add2+=add2;
}
void pushdown(int u){
change(u<<1,tr[u].add1,tr[u].add2,tr[u].cov);
change(u<<1|1,tr[u].add1,tr[u].add2,tr[u].cov);
tr[u].cov=-1,tr[u].add1=tr[u].add2=0;
}
void build(int u,int l,int r){
tr[u]={l,r,0,0,-1,0,0};
if(l==r) return;
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
void modify1(int u,int l,int r,int add1,int add2){
if(l>r) return;
if(tr[u].l>=l&&tr[u].r<=r){
change(u,add1,add2,-1);
return;
}
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify1(u<<1,l,r,add1,add2);
if(r>mid) modify1(u<<1|1,l,r,add1,add2);
pushup(u);
}
void modify2(int u,int l,int r,int x){
if(l>r) return;
if(tr[u].l>=l&&tr[u].r<=r){
if(tr[u].mi>x){
change(u,0,0,x);
return;
}
if(tr[u].mx<=x) return;
if(tr[u].l==tr[u].r) return;
}
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify2(u<<1,l,r,x);
if(r>mid) modify2(u<<1|1,l,r,x);
pushup(u);
}
int query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].mi;
pushdown(u);
int mid=tr[u].l+tr[u].r>>1,mi=1e18;
if(l<=mid) mi=min(mi,query(u<<1,l,r));
if(r>mid) mi=min(mi,query(u<<1|1,l,r));
return mi;
}
}tree;
signed main(){
cin >> n >> C;
for(int i=1; i<=n; i++) cin >> a[i];
memcpy(b,a,sizeof a);
sort(b+1,b+n+1);
reverse(b+1,b+n+1);
tree.build(1,1,n);
for(int i=1; i<=n; i++){
int l=1,r=n;
while(l<r){
int mid=(l+r+1)/2;
if(b[mid]>=a[i]) l=mid;
else r=mid-1;
}
tree.modify1(1,1,l,-a[i],1);
tree.modify1(1,l+1,n,C,0);
tree.modify2(1,l+1,n,tree.query(1,l,l));
}
cout << tree.query(1,n,n) << endl;
return 0;
}
\(5\):\(P4284\)
题解:考虑树形\(dp\),设\(f_i\)为最终第\(i\)个元件导电概率,答案即为\(\sum f_i\)。
一个原件有三种途径获得电:
\(1\):自己来电了。
\(2\):其子树里有一节点导电传了过来。
\(3\):其子树外有一节点导电传了过来。
第一种情况可以简单讨论。
对于根节点,显然没有第三种情况,于是令\(f_i\)为只考虑前两种情况第\(i\)个元件导电概率。
显然\(f_i\)不为最终\(i\)的导电概率,但对于根节点,\(f_i\)恰好为最终\(i\)的导电概率。
于是考虑换根\(dp\),累加所有节点为根时的\(f_i\)即为答案。
有两事件\(A,B\),至少发生任意一件的概率为\(P(A)+P(B)-P(A)P(B)\)。
用子节点更新父节点,有转移\(f_i^{'}=f_i+f_j*p_{i,j}-f_i*f_j*p_{i,j}\)。
换根的基础是撤销贡献,考虑撤销贡献。
根据\(f_i^{'}=f_i+f_j*p_{i,j}-f_i*f_j*p_{i,j}\)。解得\(f_i=\frac{f_i^{'}-f_j*p_{i,j}}{1-f_j*p_{i,j}}\)。
然后转移\(f_j^{'}=f_j+f_i*p_{i,j}-f_j*f_i*p_{i,j}\),即可。
但有一个细节,在反解时,出现了分母,而分母可能为零。
但分母为零,意味着\(f_j\)为\(1\),此时\(i\)对\(j\)的更新就无意义了,直接忽略即可。
#include <bits/stdc++.h>
#define double long double
using namespace std;
const int N=5e5+10;
int n;
double ans,w[2*N],p[N],f[N];
int h[N],e[2*N],ne[2*N],idx;
void add(int a,int b,double c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void dfs1(int u,int fa){
f[u]=p[u];
for(int i=h[u]; i!=-1; i=ne[i]){
int j=e[i];
if(j==fa) continue;
dfs1(j,u);
f[u]=f[u]+f[j]*w[i]-f[u]*f[j]*w[i];;
}
}
void dfs2(int u,int fa){
ans+=f[u];
for(int i=h[u]; i!=-1; i=ne[i]){
int j=e[i];
if(j==fa) continue;
if(f[j]*w[i]!=1){
f[u]=(f[u]-f[j]*w[i])/(1-f[j]*w[i]);
f[j]=f[j]+f[u]*w[i]-f[j]*f[u]*w[i];
}
dfs2(j,u);
if(f[u]*w[i]!=1){
f[j]=(f[j]-f[u]*w[i])/(1-f[u]*w[i]);
f[u]=f[u]+f[j]*w[i]-f[u]*f[j]*w[i];
}
}
}
int main(){
memset(h,-1,sizeof h);
cin >> n;
for(int i=1; i<=n-1; i++){
int a,b;
double c;
cin >> a >> b >> c;
c/=100.0;
add(a,b,c),add(b,a,c);
}
for(int i=1; i<=n; i++) cin >> p[i];
for(int i=1; i<=n; i++) p[i]/=100.0;
dfs1(1,-1);
dfs2(1,-1);
printf("%.6Lf\n",ans);
return 0;
}
\(6\):\(P8746\)
https://www.luogu.com.cn/problem/P8746
题意:有\(n\)个人,\(m\)种糖果,每种糖果有\(2\)个,且有价值\(a_i\)。
给每个人分配一段区间,获得区间内每种糖果一个,设每个人获得糖果总价值为\(v_i\),要求最小化\(max\{v_i\}-min\{v_i\}\)。
题解:由于最小值有上界\(O(\frac{2sum}{m})\),于是考虑枚举最小值,去求最大值。
显然不存在区间\([l_1,r_1],[l_2,r_2]\),使得\(l_1<l_2<r_2<r_1\)。
这是因为将两区间换成\([l_1,r_2],[l_2,r_1]\),不会使得答案变劣。
考虑\(dp\),令\(f(i,j,k)\)为取了\(i\)个区间,上一个取一颗糖果位置为\(j\),上一个取两颗糖果位置为\(k\),\(j\geq k\)的最优方案。
两颗糖果位置不取,\(f(i,j,k-1)\rightarrow f(i,j,k)\),说明\(f(i,j)\)关于\(k\)单调递减。
在\(t\leq k\)时取,\(max\{f(i-1,k,t),s_j-s_t\}\rightarrow f(i,j,k)\)。
当\(t>k\)时,这样一定会由若干转移\(1\)和一次转移\(2\)等价得到,于是可以不转移。
考虑优化转移\(2\),发现\(max\)内两项均关于\(t\)单调,于是选择尽可能大的满足\(s_j-s_t\geq mi\)的\(t\),于是先枚举\(i,k\)两维,对\(j\)用双指针维护。
#include <bits/stdc++.h>
using namespace std;
const int N=110;
int n,m;
int a[N],s[N],f[N/2][N][N];
int solve(int mi){
memset(f,0x3f,sizeof f);
f[0][0][0]=mi;
for(int i=1; i<=n; i++)
for(int k=0; k<=m; k++){
int t=0;
for(int j=k; j<=m; j++){
if(k) f[i][j][k]=min(f[i][j][k],f[i][j][k-1]);
while(t+1<=k&&s[j]-s[t+1]>=mi) t++;
//cout << t << endl;
if(s[j]-s[t]>=mi) f[i][j][k]=min(f[i][j][k],max(f[i-1][k][t],s[j]-s[t]));
}
}
//cout << mi << " " << f[n][m][m] << endl;
return f[n][m][m];
}
int main(){
cin >> m >> n;
for(int i=1; i<=m; i++) cin >> a[i];
for(int i=1; i<=m; i++) s[i]=s[i-1]+a[i];
int ans=1e9;
for(int i=1; i<=2*s[m]/n; i++) ans=min(ans,solve(i)-i);
cout << ans << endl;
return 0;
}
\(7\):\(P10694\)
https://www.luogu.com.cn/problem/P10694
题意:给定字符串\(S\),以及字符串\(s_1,s_2\)。一个字符串\(S\)的贡献为\(s_1\)作为子序列在\(S\)中出现次数,与\(s_2\)作为子序列在\(S\)中出现次数的乘积。现在请你求出\(S\)所有子串的贡献之和。
题解:经典拆贡献,考虑两个子序列对多少串产生贡献,子串只要完整包含两个子序列,就会产生\(1\)的贡献。
设两子序列为左右端点为\(l_1,r_1,l_2,r_2\),则会产生\(min\{l_1,l_2\}*(n-max\{r_1,r_2\}+1)\)的贡献。
于是考虑对子序列右端点扫描线,在\(max\{r_1,r_2\}\)处统计贡献。
考虑\(dp\),令\(f(i,j,k)\)为当前\(S,s_1,s_2\)最后一个完成匹配的分别是其第\(i,j,k\)个字符的方案的\(min\{l_1,l_2\}\)的和。
有转移\(f(i,j,k)=[S_i=s_{1,j}]\sum_{x=0}^{i-1}f(x,j-1,k)+[S_i=s_{2,k}]\sum_{x=0}^{i-1}f(x,j,k-1)+[S_i=s_{1,j}=s_{2,k}]\sum_{x=0}^{i-1}f(x,j-1,k-1)\),前缀和优化即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10,M=21,P=998244353;
int n,m1,m2;
int f[N][M][M],g[M][M];
char S[N],s1[M],s2[M];
int main(){
scanf("%s",S+1),n=strlen(S+1);
scanf("%s",s1+1),m1=strlen(s1+1);
scanf("%s",s2+1),m2=strlen(s2+1);
int ans=0;
for(int i=1; i<=n; i++){
for(int j=0; j<=m1; j++)
for(int k=0; k<=m2; k++){
if(j&&s1[j]==S[i]){
f[i][j][k]=(f[i][j][k]+g[j-1][k])%P;
if(j==1&&!k) f[i][j][k]=(f[i][j][k]+i)%P;
}
if(k&&s2[k]==S[i]){
f[i][j][k]=(f[i][j][k]+g[j][k-1])%P;
if(!j&&k==1) f[i][j][k]=(f[i][j][k]+i)%P;
}
if(j&&k&&s1[j]==S[i]&&s2[k]==S[i]){
f[i][j][k]=(f[i][j][k]+g[j-1][k-1])%P;
if(j==1&&k==1) f[i][j][k]=(f[i][j][k]+i)%P;
}
if(j==m1&&k==m2) ans=(ans+(LL)f[i][j][k]*(n-i+1))%P;
}
for(int j=0; j<=m1; j++)
for(int k=0; k<=m2; k++)
g[j][k]=(g[j][k]+f[i][j][k])%P;
}
cout << ans << endl;
return 0;
}
\(8\):黄金替罪羊
https://codeforces.com/gym/105887
题意:给定一个数轴和长\(2n\)的由\(LR?\)构成的序列,小\(a\)从第\(1\)秒开始按指令走,小\(b\)从第\(n+1\)秒按指令走,从第\(n\)秒开始,若行动后小\(a\)和小\(b\)相遇,则序列非法,求将\(?\)替换为\(L\)或\(R\)后,合法序列方案数。
题解:直接考虑\(dp\),令\(f(t,i,j)\)为在第\(t\)秒后,小\(a\)走到\(i\),小\(b\)走到\(j\)的合法序列方案数。
但这样有一个明显的缺陷,就是小\(b\)所走的路,应该和小\(a\)前\(n\)步走的路相同,但小\(a\)所走的具体方案,却是无法记录的。
那不妨将小\(a\)前\(n\)步所走方案以第\(n\)步后所在位置为代表元合并,那枚举代表元,此时小\(b\)可以任意行走,只需最后所到位置也为代表元即可。
#include <bits/stdc++.h>
using namespace std;
const int N=110,P=998244353;
int n,ans;
int f[N][2*N][N];
char s[N];
int main(){
cin >> n;
for(int i=1; i<=2*n; i++) cin >> s[i];
for(int k=-n; k<=n; k++){
if(k==0) continue;
memset(f,0,sizeof f);
f[n][k+2*n+1][n+1]=1;
for(int t=n+1; t<=2*n; t++)
for(int i=1; i<=4*n+1; i++)
for(int j=1; j<=2*n+1; j++){
if(i-n==j) continue;
if((s[t-n]=='L'||s[t-n]=='?')&&(s[t]=='L'||s[t]=='?'))
f[t][i][j]=(f[t][i][j]+f[t-1][i+1][j+1])%P;
if((s[t-n]=='L'||s[t-n]=='?')&&(s[t]=='R'||s[t]=='?'))
f[t][i][j]=(f[t][i][j]+f[t-1][i-1][j+1])%P;
if((s[t-n]=='R'||s[t-n]=='?')&&(s[t]=='L'||s[t]=='?'))
f[t][i][j]=(f[t][i][j]+f[t-1][i+1][j-1])%P;
if((s[t-n]=='R'||s[t-n]=='?')&&(s[t]=='R'||s[t]=='?'))
f[t][i][j]=(f[t][i][j]+f[t-1][i-1][j-1])%P;
}
for(int i=1; i<=4*n+1; i++) ans=(ans+f[2*n][i][k+n+1])%P;
}
cout << ans << endl;
return 0;
}
\(9\):怯战蜥蜴
题意:给定一棵\(n\)个点的树,每个点有\(p_i\)概率为\(1\),对于一种情况,其权值为选择一个叶子走到根,可能的最小点权和。
对于根为\(1,2,...,n\),分别求出权值的期望。
题解:考虑换根\(dp\),一种显然的想法是令\(f(i,j)\)为仅考虑\(i\)为根子树,走到\(i\)的最小权值为\(j\)的概率,但实践发现这样设状态,极其难以实现。
考虑令\(f(i,j)\)为仅考虑以\(i\)为根子树,走到\(i\)的最小权值不小于\(j\)的概率。
显然有转移式,\(f(i,j)=(1-p_i)\prod f(son,j)+p_i\prod f(son,j-1)\),其中\(son\)为\(i\)的儿子。
设以\(rt\)为根时,答案为\(\sum_{i=1}^{n} i(f(rt,i)-f(rt,i+1))=\sum_{i=1}^{n} f(rt,i)\)。
直接进行换根\(dp\)即可,但由于涉及乘法,则逆操作需要求逆元,所以复杂度为\(O(n^2logn)\),不一定能通过。
以下介绍一种\(O(n^2)\)的做法。
令\(g(i,j)\)为在以\(i\)为根节点时,仅考虑以\(i\)的原父亲\(fa_i\)为根的子树,走到\(fa_i\)的最小权值不小于\(j\)的概率。
令\(son(i,j)\)为\(i\)的第\(j\)个儿子。
令\(pre(i,j,k)=\prod f(son(i,t),j),1\leq t\leq k\)。
令\(suf(i,j,k)=\prod f(son(i,t),j),k \leq t\leq sz_i\)。
则当\(i\)为根时,答案为\(\sum_{j=1}^{n}(1-p_i)pre(i,j,sz_i)g(i,j)+p_ipre(i,j-1,sz_i)g(i,j-1)\)。
而对于\(g\)的求解,可以从上至下递推,具体来说,有递推式:
\(g(son(i,t),j)=(1-p_i)g(i,j)pre(i,j,t-1)suf(i,j,t+1)+p_ig(i,j-1)pre(i,j-1,t-1)suf(i,j-1,t+1)\)。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=3010,P=998244353;
int n,rt;
int p[N],sz[N],deg[N];
int f[N][N],g[N][N];
int h[N],e[2*N],ne[2*N],idx;
vector<int> son[N],pre[N][N],suf[N][N];
int cnt;
int qpow(int a,int b){
int res=1;
while(b){
if(b&1) res=(LL)res*a%P;
a=(LL)a*a%P;
b>>=1;
}
return res;
}
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa){
sz[u]=0x3f3f3f3f;
for(int i=h[u]; i!=-1; i=ne[i]){
int j=e[i];
if(j==fa) continue;
dfs(j,u);
sz[u]=min(sz[u],sz[j]+1);
}
if(deg[u]==1) sz[u]=1;
}
void dfs1(int u,int fa){
for(int i=h[u]; i!=-1; i=ne[i]){
int j=e[i];
if(j==fa) continue;
dfs1(j,u);
son[u].push_back(j);
for(int k=0; k<=sz[u]; k++,cnt++){
pre[u][k].push_back(f[j][k]);
suf[u][k].push_back(f[j][k]);
}
}
for(int i=0; i<=sz[u]; i++)
for(int j=1; j<son[u].size(); j++,cnt++)
pre[u][i][j]=(LL)pre[u][i][j]*pre[u][i][j-1]%P;
for(int i=0; i<=sz[u]; i++)
for(int j=son[u].size()-2; j>=0; j--,cnt++)
suf[u][i][j]=(LL)suf[u][i][j]*suf[u][i][j+1]%P;
if(deg[u]==1) f[u][0]=1,f[u][1]=p[u];
else{
f[u][0]=1;
for(int i=1; i<=sz[u]; i++,cnt++){
f[u][i]=(f[u][i]+(LL)(1-p[u])*pre[u][i][son[u].size()-1])%P;
f[u][i]=(f[u][i]+(LL)p[u]*pre[u][i-1][son[u].size()-1])%P;
}
}
}
void dfs2(int u){
for(int t=0; t<son[u].size(); t++){
int j=son[u][t];
g[j][0]=1;
for(int k=1; k<=sz[u]; k++,cnt++){
g[j][k]=(g[j][k]+(LL)(1-p[u])*(t?pre[u][k][t-1]:1)
%P*(t+1<son[u].size()?suf[u][k][t+1]:1)%P*g[u][k])%P;
g[j][k]=(g[j][k]+(LL)p[u]*(t?pre[u][k-1][t-1]:1)
%P*(t+1<son[u].size()?suf[u][k-1][t+1]:1)%P*g[u][k-1])%P;
}
dfs2(j);
}
}
int main(){
memset(h,-1,sizeof h);
cin >> n;
for(int i=1; i<=n; i++) cin >> p[i];
for(int i=1; i<=n; i++){
int x;
cin >> x;
p[i]=(LL)p[i]*qpow(x,P-2)%P;
}
for(int i=1; i<=n-1; i++){
int a,b;
cin >> a >> b;
deg[a]++,deg[b]++;
add(a,b),add(b,a);
}
if(n==2){
cout << p[1] << " " << p[2] << endl;
return 0;
}
for(int i=1; i<=n; i++)
if(deg[i]!=1){
rt=i;
break;
}
dfs(rt,-1);
dfs1(rt,-1);
for(int i=0; i<=n; i++) g[rt][i]=1;
dfs2(rt);
for(int i=1; i<=n; i++){
if(deg[i]==1){
cout << p[i] << " ";
continue;
}
int ans=0;
for(int j=1; j<=sz[i]; j++,cnt++){
ans=(ans+(LL)(1-p[i])*g[i][j]%P*pre[i][j][son[i].size()-1])%P;
ans=(ans+(LL)p[i]*g[i][j-1]%P*pre[i][j-1][son[i].size()-1])%P;
}
ans=(ans%P+P)%P;
cout << ans << " ";
}
cout << endl;
return 0;
}
\(10\):\(CF2107F1\)
https://www.luogu.com.cn/problem/CF2107F1
题意:在你前面有\(n\)个骑手,第\(i\)个人有权值\(a_i\),你可以进行如下两种操作:
\(1\):设当且在第\(i\)个人后面,花费\(a_i\)代价超车。
\(2\):花费\(|i-j|\)的代价,交换\(a_i\)和\(a_j\)。
题解:设当前在\(i\)后面,若要将\(a_i\)和\(a_j\)交换,则一定需要满足\(a_j\)是\([j,i]\)里的严格最小值,且交换后,在\([j,i]\)里,都需要拿\(a_j\)操作。
这是因为,若在\([j+1,i]\)里存在一个小于等于\(a_j\)的数,那拿这个数操作,一定比拿\(a_j\)操作更优秀。
而每次超车后,可以花费\(a_j+1\)代价继续使用\(a_j\),而由于\(a_j\)是区间严格最小值,故一定最优。
而超车过程中,一定由若干段构成,每段都用相同的数操作。
所以不妨考虑\(dp\),设\(f(i)\)为当前在\(i\)后面,想要走到最前面所需最小代价。
每次枚举向前延展的段,根据先前分析,这段里最优的抉择,一定是选择一直操作最小的数。
所以\(f(i)=min\{f(j)+(i-pos)+(i-(j+1))+val(i-j)\)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5010;
int n;
int a[N],f[N];
int lg[N],g[N][20];
int get(int l,int r){
int k=lg[r-l+1];
if(a[g[l][k]]<a[g[r-(1<<k)+1][k]]) return g[l][k];
return g[r-(1<<k)+1][k];
}
signed main(){
lg[0]=-1;
for(int i=1; i<N; i++) lg[i]=lg[i/2]+1;
int T;
cin >> T;
while(T--){
cin >> n;
for(int i=1; i<=n; i++) cin >> a[i];
for(int i=1; i<=n; i++) g[i][0]=i;
for(int j=1; j<=16; j++)
for(int i=1; i+(1<<j)-1<=n; i++)
if(a[g[i][j-1]]<a[g[i+(1<<(j-1))][j-1]]) g[i][j]=g[i][j-1];
else g[i][j]=g[i+(1<<(j-1))][j-1];
f[0]=0;
for(int i=1; i<=n; i++){
f[i]=1e18;
for(int j=0; j<=i-1; j++){
int p=get(j+1,i);
f[i]=min(f[i],f[j]+(i-p)+(i-(j+1))+a[p]*(i-j));
}
}
cout << f[n] << endl;
}
return 0;
}
浙公网安备 33010602011771号