有趣的DP题

1.Min酱要旅行
如果枚举每个选不选然后进行背包显然不行
反着考虑
\(g_{i,j}=背包中不选物品i时体积为j的所有情况=背包中物品体积为j的所有情况-选择物品i时体积为j的所有情况\)
\(f_j=背包中物品体积为j的所有情况\)
因此\(g_{i,j}=f_j-g_{i,j-k_i}\)

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=20020219;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}                                          
int n,m;
void solve(){
    vector<ll>k(n+1),f(m+1);
    for(int i=1;i<=n;i++)k[i]=read();
    f[0]=1;
    for(int i=1;i<=n;i++){
        for(int j=m;j>=k[i];j--)f[j]=(f[j]+f[j-k[i]])%10;
    }
    for(int i=1;i<=n;i++){
        vector<ll>g(m+1);
        for(int j=0;j<k[i];j++)g[j]=f[j];
        for(int j=k[i];j<=m;j++)g[j]=(f[j]-g[j-k[i]])%10;
        for(int j=1;j<=m;j++)printf("%lld",(g[j]%10+10)%10);puts("");
    }
}
int main(){ 
    while(scanf("%d%d",&n,&m)!=EOF)solve();
    return 0;
}

2.[HAOI2011]PROBLEM A
正难则反
每个人说真话\(a_i,b_i\)代表一个区间[l,r]\((l=a_i+1,r=n-b_i)\),表示排名等于l,共有r-l+1人也是排名为l
\(dp_i\)表示前i排名的说真话人个数
\(dp_i=max_j (dp_j+sum[j+1][i])\)
\(sum[l][r]\)表示说真话排名在[l,r]区间人个数
那么如果\(sum[l][r]\leq r-l+1\),还能保证对吗?
因为题目肯定有解,说真话人就那么多,不够的用说谎话的人补上,一定能对的
因为数组开不下,用vector和map搭配

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=20020219;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}      
int n;
map<pa,int>mm;
int main(){ 
    n=read();
    vector<vector<pa> >t(n+1);
    for(int i=1;i<=n;i++){
        int x=read(),y=read();
        int l=x+1,r=n-y;
        if(l>r)continue;
        mm[mp(l,r)]++;
        mm[mp(l,r)]=min(mm[mp(l,r)],r-l+1);
        if(mm[mp(l,r)]==1)t[r].pb(mp(l,r));
    }
    vector<int>dp(n+1);
    for(int i=1;i<=n;i++){
        dp[i]=dp[i-1];
        for(auto j:t[i])dp[i]=max(dp[i],dp[j.fi-1]+mm[j]);
    }
    printf("%d",n-dp[n]);
    return 0;
}

3. [NOI2009]管道取珠
这里\(\sum a_i^2\)很难处理
有一个很妙的转化,处理方案数量平方的优化可以看成两个人玩同一个游戏,他们输出序列一样的种类数。
\(a_i*a_i\)就相当于第一组有\(a_i\)种可能形成i序列和第二组有\(a_i\)种可能形成i序列的种类相乘
dp[i1][j1][i2][j2]表示第一组取了i1个up,j1个down,第二组取了i2个up,j2个down时,第一组和第二组相同的方案数
i1+j1==i2+j2,省去一维j2=i1+j1-i2
然后就是分类讨论

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=1024523;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}      
int n,m,dp[505][505][505];
//dp[i1][j1][i2][j2]表示第一组取了i1个up,j1个down,第二组取了i2个up,j2个down时,第一组和第二组相同的方案数
//i1+j1==i2+j2,省去一维j2=i1+j1-i2
int main(){ 
    n=read();m=read();
    char a[n+1],b[m+1];
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=m;i++)cin>>b[i];
    reverse(a+1,a+n+1);reverse(b+1,b+m+1);
    dp[0][0][0]=1;
    for(int i1=0;i1<=n;i1++)for(int j1=0;j1<=m;j1++){
        for(int i2=0;i2<=n;i2++){
            int j2=i1+j1-i2;
            if(!dp[i1][j1][i2] || j2>m || j2<0)continue;
            ll x=dp[i1][j1][i2];
            //分类讨论来转移方程
            if(a[i1+1]==a[i2+1])(dp[i1+1][j1][i2+1]+=x)%=MOD;
            if(a[i1+1]==b[j2+1])(dp[i1+1][j1][i2]+=x)%=MOD;
            if(b[j1+1]==a[i2+1])(dp[i1][j1+1][i2+1]+=x)%=MOD;
            if(b[j1+1]==b[j2+1])(dp[i1][j1+1][i2]+=x)%=MOD;
        }
    }
    printf("%d\n",(dp[n][m][n]%+MOD)%MOD);
    return 0;
}

4.灯谜
\(E(x^3)2^m\)一看很复杂,但是把\(2^m\)带进\(E(x^3)\)
就当与求\(\sum x^3\),从上一道题得到的思路,试试带入这道题
考虑怎么计算\(x^3\),借鉴上一道题,我们进行3次这样的游戏,每次都是独立的
\(x=\sum x_i= x_1+x_2+···+x_i+···+x_n,x_i=0/1表示第i个灯是关还是开\)
\(\sum x^3=\sum (\sum x_i)*(\sum x_j)*(\sum x_k)\)
所以当任意\(i,j,k\)使得\(x_i*x_j*x_k=1\)对答案有贡献
我们枚举\(i,j,k\),找出\(x_i*x_j*x_k=1\)的方案数
\(dp_{i,st}\)表示前i个按钮,枚举的\(i,j,k\)亮灯状态为st的方案数
第i个按钮不按:\(dp_{i,st}=dp_{i-1,st}\)
按:\(dp_{i,st}+=dp_{i-1,st'}\),st为st'在第i个按钮按发生变化后的状态

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=1e9+7;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}      

int n,m,bk[51][51];
ll dp[51][8];
ll get(int x,int y,int z){
    memset(dp,0,sizeof(dp));
    dp[0][0]=1;
    for(int i=1;i<=m;i++)for(int j=0;j<8;j++){
        dp[i][j]+=dp[i-1][j];dp[i][j]%=MOD;
        int now=j;
        if(bk[i][x])now^=1;
        if(bk[i][y])now^=2;
        if(bk[i][z])now^=4;
        dp[i][j]+=dp[i-1][now];dp[i][j]%=MOD;
    }
    return dp[m][7];
}
int main(){ 
    n=read();m=read();
    for(int i=1;i<=m;i++){
        int k=read();
        for(int j=1;j<=k;j++)bk[i][read()]=1;
    }
    ll ans=0;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)for(int k=1;k<=n;k++){
        ans+=get(i,j,k);
        ans%=MOD;
    }
    printf("%lld",ans);
    return 0;
}

5.F - Monochromatic Path
题意:\(h*w\)的棋盘上,每个格子是黑色或者白色,行i或者列j的颜色取反分别花费\(r_i,c_j\)的费用
问最小花费,使得从\((1,1)到(h,w)\)存在一条路径,且路径上的颜色一样
题解:
这道题dp设出来不难,主要是转移
\(dp_{i,j,st}\)表示从\((1,1)\)同颜色走到\((i,j)\)的最小花费
\(st=0\)表示\((i,j)\)点没取反
\(st=1\)表示到\((i,j)\)点,第i行取反,花费了\(r_i\)
\(st=2\)表示到\((i,j)\)点,第j列取反,花费了\(c_j\)
\(st=3\)表示到\((i,j)\)点,第i行取反,且第j列也取反,花费了\(r_i+c_j\)
考虑向下走从\((i,j)\)转移到\((i+1,j)\),向右走同理
\(a_{i,j}=a_{i+1,j}\)
那么
\(dp_{i+1,j,0}=dp_{i,j,0}\)
\(dp_{i+1,j,1}=dp_{i,j,1}+r[i+1]\)
\(dp_{i+1,j,2}=dp_{i,j,2}\)
\(dp_{i+1,j,3}=dp_{i,j,3}+r[i+1]\)
\(a_{i,j}\not =a_{i+1,j}\)
那么
\(dp_{i+1,j,0}=dp_{i,j,0}+r[i+1]\)
\(dp_{i+1,j,1}=dp_{i,j,1}\)
\(dp_{i+1,j,2}=dp_{i,j,2}+r[i+1]\)
\(dp_{i+1,j,3}=dp_{i,j,3}\)

转移用位运算,很巧妙,见代码

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
using namespace std;
const int maxn=3e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int h,w;
ll r[maxn],c[maxn];
int a[2005][2005];
ll dp[2005][2005][4];
int main(){
    h=read();w=read();
    for(int i=1;i<=h;i++)r[i]=read();
    for(int i=1;i<=w;i++)c[i]=read();
    for(int i=1;i<=h;i++){
        string s;cin>>s;
        for(int j=1;j<=w;j++){
            if(s[j-1]=='0')a[i][j]=0;
            else a[i][j]=1;
        }
    }
    for(int i=0;i<=h;i++)for(int j=0;j<=w;j++)for(int k=0;k<=3;k++)dp[i][j][k]=4e12+101;
    dp[1][1][0]=0;
    dp[1][1][1]=r[1];
    dp[1][1][2]=c[1];
    dp[1][1][3]=r[1]+c[1];
    for(int i=1;i<=h;i++)for(int j=1;j<=w;j++){
        for(int k=0;k<4;k++){
            if(i<h){
                int k2=k;
                if(a[i][j]!=a[i+1][j])k2^=1;
                if(k2&1)dp[i+1][j][k2]=min(dp[i+1][j][k2],dp[i][j][k]+r[i+1]);
                else dp[i+1][j][k2]=min(dp[i+1][j][k2],dp[i][j][k]);
            }
            if(j<w){
                int k2=k;
                if(a[i][j]!=a[i][j+1])k2^=2;
                if(k2&2)dp[i][j+1][k2]=min(dp[i][j+1][k2],dp[i][j][k]+c[j+1]);
                else dp[i][j+1][k2]=min(dp[i][j+1][k2],dp[i][j][k]);
            }
        }
    }
    ll ans=dp[0][0][0];
    for(int i=0;i<4;i++)ans=min(ans,dp[h][w][i]);
    cout<<ans;
    return 0;
}

6.[AHOI2009]CHESS 中国象棋
每行最多两个炮,每列也最多两个——N,M<=8 可以用三进制状压按行转移
更大在怎么办?
10021和11200这两个状态在转移上有区别么?也就是说:之前究竟是哪一列被放了1/2个
炮重要吗?
只需要知道有多少列放了两个炮,多少列放了一个就行了
\(dp_{i,j,k}\)表示前i行,有j列没放,k列放1个,m-j-k列放2个
每次转移三种情况,见代码

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=9999973;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}

ll n,m,dp[103][113][113];
//dp[i][j][k]=前i行有j列没放,k列放1个,m-j-k列放2个 
int main(){
	n=read();m=read();
	dp[0][m][0]=1;
	for(int i=0;i<=n;i++)for(ll j=0;j<=m;j++)for(ll k=0;k<=m;k++){
		if(k+j>m)continue;
		//i+1行不放
		dp[i+1][j][k]+=dp[i][j][k];dp[i+1][j][k]%=MOD;
		//i+1行放1个 
		if(j-1>=0)dp[i+1][j-1][k+1]+=dp[i][j][k]*j,dp[i+1][j-1][k+1]%=MOD;  //放在空列 
		if(k-1>=0)dp[i+1][j][k-1]+=dp[i][j][k]*k,dp[i+1][j][k-1]%=MOD; 		//放在有1个的列 
		//i+1行放2个 
		if(j-2>=0)dp[i+1][j-2][k+2]+=dp[i][j][k]*j*(j-1)/2,dp[i+1][j-2][k+2]%=MOD; //2个都放在空列 
		if(j-1>=0 && k-1>=0)dp[i+1][j-1][k]+=dp[i][j][k]*j*k,dp[i+1][j-1][k]%=MOD; //2个都放在有1个的列 
		if(k-2>=0)dp[i+1][j][k-2]+=dp[i][j][k]*k*(k-1)/2,dp[i+1][j][k-2]%=MOD;	   //2个都放在有2个的列 
	}
	ll ans=0;
	for(int j=0;j<=m;j++)for(int k=0;k<=m;k++){
        if(j+k>m)continue;
        ans+=dp[n][j][k],ans%=MOD;
    }
	printf("%lld\n",(ans%MOD+MOD)%MOD);
    return 0;
}

7.区间价值
首先找找性质:(假设序列为 1 1 2 3 4 4 5)
区间长度从3变成4
112变成112 3,+1
123变成123 4,+1
234变成234 4,+0
344变成344 5,+1
445没有了,-2
其实+1和+0的区别就是 当前新加的数的位置 和 前一个与自己相同数的位置 之差要大于等于4
最后再减去后3个数的价值
那么我们设dp[i]表示长度所有长度为i的价值和
dp[i]=dp[i-1]+delta[i]-diff[i-1]
delta[i]=所有与上一个相同数字距离大于等于i的个数,后缀和
diff[i-1]表示长度为i-1的最后一个区间的价值

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
ll n,a[maxn],dp[maxn],delta[maxn];
//dp[i]表示长度所有长度为i的价值和
//dp[i]=dp[i-1]+delta[i]-diff[i-1]
//delta[i]=所有与上一个相同数字距离大于等于i的个数
//diff[i-1]表示长度为i-1的最后一个区间的价值 
int main(){
	n=read();
	for(int i=1;i<=n;i++)a[i]=read();
	map<int,int>last;
	for(int i=1;i<=n;i++){
		delta[i-last[a[i]]]++;
		last[a[i]]=i;
	} 
	for(int i=n;i;i--)delta[i]+=delta[i+1];
	ll diff=0;map<int,int>ji;
	for(int i=1;i<=n;i++){
		dp[i]=dp[i-1]+delta[i]-diff;
		if(ji[a[n-i+1]])continue;
		diff++;ji[a[n-i+1]]=1;
	}
	int q=read();
	while(q--)printf("%lld\n",dp[read()]);
    return 0;
}

8.排列游戏
很有意思的题,虽然一看就是dp题,但是让你难以下手的原因是我们无法保证每个数只出现一次且满足条件
我们设\(dp[i][j]表示前i个数,最后一个是j的方案数,并且前i个数都是小于等于i\)
\(sum[i][j]=dp[i][1]+dp[i][2]+···+dp[i][j]\)

  1. a[i-1]=1
    dp[i][j]=sum[i-1][j-1]
  2. a[i-1]=2
    dp[i][j]=sum[i-1][i]-sum[i-1][j-1]
  3. a[i-1]=0
    dp[i][j]=sum[i-1][i]

为什么这样是对的?
把(1,i-1)中大于等于j的变成j+1,这样既不破坏增减性又不影响结果数目————实质是满足了无后效性
很巧妙
对于类似计数题(保证每个数只出现一次)可以用类似的思想,我们可以将前面的数+1之类的方式来求解

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e3+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int n,a[maxn];
int dp[maxn][maxn],sum[maxn][maxn];
/*
dp[i][j]表示前i个数,最后一个是j的方案数,并且前i个数都是小于等于i的
sum[i][j]=dp[i][1]+dp[i][2]+···+dp[i][j] 
a[i-1]==1 
	dp[i][j]=sum[i-1][j-1]
a[i-1]==2
	dp[i][j]=sum[i-1][i]-sum[i-1][j-1]
a[i-1]==0
	dp[i][j]=sum[i-1][i]

*/
int main(){
	string s;cin>>s;n=s.length();
	for(int i=1;i<=n;i++)a[i]=(int)(s[i-1]-'0');
	dp[1][1]=1;for(int i=1;i<=n;i++)sum[1][i]=1;
	for(int i=2;i<=n+1;i++){
		for(int j=1;j<=i;j++){
			if(a[i-1]==1)dp[i][j]=sum[i-1][j-1];
			if(a[i-1]==2)dp[i][j]=sum[i-1][i]-sum[i-1][j-1];
			if(a[i-1]==0)dp[i][j]=sum[i-1][i];
			dp[i][j]%=MOD;
		}
		for(int j=1;j<=n+1;j++){
			sum[i][j]+=sum[i][j-1]+dp[i][j];
			sum[i][j]%=MOD;
		} 
	}
	printf("%lld\n",(sum[n+1][n+1]%MOD+MOD)%MOD);
    return 0;
}

9. [ZJOI2008]骑士
首先读完这道题很容易想到跟“没有上司的舞会”这道树形dp很像
但仔细一想,这道题是有向图其实也可以看作无向图,所以可能存在环和多个连通块。
所以可以把环分开,因为两个端点矛盾所以两个端点分别dfs,累加每个连通块的最大值

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
ll n,a[maxn],dp[maxn][2];
int tot,head[maxn],to[maxn],nx[maxn];
void add(int x,int y){to[++tot]=y;nx[tot]=head[x];head[x]=tot;}
int f[maxn];
int find(int x){
	if(f[x]==x)return x;
	return f[x]=find(f[x]);
}
int vis[maxn];
void dfs(int x,int val){
	dp[x][0]=0;dp[x][1]=a[x];vis[x]=val;
	for(int i=head[x];i;i=nx[i]){
		int v=to[i];if(vis[v]==val)continue;
		dfs(v,val);
		dp[x][0]+=max(dp[v][0],dp[v][1]);
		dp[x][1]+=dp[v][0]; 
	}
	return;
}
int main(){
	n=read();vector<pa>info;
	for(int i=1;i<=n;i++)f[i]=i;
	for(int i=1;i<=n;i++){
		a[i]=read();int x=read();
		add(i,x);add(x,i);
		int ii=find(i),xx=find(x);
		if(ii==xx)info.pb(mp(i,x));
		f[ii]=xx;
	}
	ll ans=0;
	for(auto i:info){
		dfs(i.fi,1);dfs(i.fi,0);
		ll ans1=dp[i.fi][0];
		dfs(i.se,1);dfs(i.se,0);
		ans1=max(ans1,dp[i.se][0]);
		ans+=ans1;
	}
	printf("%lld\n",ans);
    return 0;
}

10. [NOI2013]快餐店
跟上一题一样是个基环外向树,我们就应该把环作为重点研究对象。
很暴力的思路就是我们可以考虑每次破掉环上的一条边使它变成一颗树,然后求出直径,最后统计所有的直径中最短的那个作为答案即可。
考虑如何优化
首先我们可以把环上任意点所在的子树的参数存放在这个点上,然后对于这些点进行操作。
我们先考虑整个图上的直径可能存在的情况。

  1. 这个直径完全在环上某一点所在的子树中。
  2. 这个直径从环上某一点所在子树出发,到达该点后在环上走过一些边到达另一个点,进入该点所在子树并且结束。

第一种情况解决比较简单,我们只需要遍历环上的每一个点,求出它所在子树的直径计入答案即可。

第二种方法,我们容易发现直径在某子树内的部分一定是从该子树的根出发的最长路径,即树的最大深度。
所以我们预先处理环上所有点所在子树的最大深度记为\(dis_i\)
然后我们定义四个数组,暂且命名为\(A,B,C,D\)。记环上点数为cnt,环上点离1号点的距离为\(pre_i\),离cnt号点的距离为\(bk_i\)
我们可以很自然的想到对于某一个i,在环上断开i和i+1之间的连边后直径应该可以分成三部分(其中tmp是1和cnt两点间边的长度):

  1. 只在1~i中的直径,记为\(B_i\),\(B_i为dis_x+pre_x+dis_y-pre_y最大值\)
  2. 只在i+1~cnt中的直径,记为\(D_i\),\(D_i为dis_x+bk_x+dis_y-bk_y最大值\)
  3. 一段在1 ~ i中,一段在i+1 ~ cnt中的直径,记为\(A_i+C_i+tmp\),\(A_i为pre_x+dis_x的最大值,C_i为bk_x+dis_x的最大值\)
    那么第二种情况的直径最小值为ans=MIN( max(B_i,D_i,A_i+C_i+tmp) )
    为什么要求最小值,因为切断某条边会导致直径变大
点击查看代码
#include <bits/stdc++.h>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=6e6+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int n,isRing[maxn],cnt,ring[maxn],val[maxn];
int tot,head[maxn],nx[maxn],to[maxn],w[maxn]; 
void add(int x,int y,int z){to[++tot]=y;nx[tot]=head[x];head[x]=tot;w[tot]=z;}
int top,dfn[maxn],fa[maxn],value[maxn];
void dfs(int x){		//找环 
	dfn[x]=++top;
	for(int i=head[x];i;i=nx[i]){
		int v=to[i];if(v==fa[x])continue;
		if(!dfn[v]){
			fa[v]=x;
			value[v]=w[i];
			dfs(v);
		}
		else if(dfn[v]>dfn[x]){
			for(;v!=x;v=fa[v]){
				isRing[v]=1;
				ring[++cnt]=v;
				val[cnt]=value[v];
			}
			isRing[x]=1;
			ring[++cnt]=x;
			val[cnt]=w[i];
		}
	}
	return ;
}
ll ans,dis[maxn];
ll A[maxn],B[maxn],C[maxn],D[maxn],pre[maxn],bk[maxn];
void get_dis(int x,int fa){
	for(int i=head[x];i;i=nx[i]){
		int v=to[i];if(isRing[v] || v==fa)continue;
		get_dis(v,x);
		ans=max(ans,dis[x]+dis[v]+w[i]);
		dis[x]=max(dis[x],dis[v]+(ll)w[i]);
	}
	return ;
} 
int main(){
	n=read();
	for(int i=1;i<=n;i++){
		int x=read(),y=read(),z=read();
		add(x,y,z);add(y,x,z);
	}
	dfs(1);
	for(int i=1;i<=cnt;i++){
		get_dis(ring[i],ring[i]); //子树深度
		pre[i+1]=pre[i]+val[i];			//环上距离前缀和 
		bk[cnt-i]=bk[cnt-i+1]+val[cnt-i];	//后缀和 
	}
	ll maxx=0;
	for(int i=1;i<=cnt;i++){
		A[i]=max(A[i-1],dis[ring[i]]+pre[i]);
		B[i]=max(B[i-1],pre[i]+dis[ring[i]]+maxx);
		maxx=max(maxx,dis[ring[i]]-pre[i]);
	} 
	maxx=0;
	for(int i=cnt;i;i--){
		C[i]=max(C[i+1],dis[ring[i]]+bk[i]);
		D[i]=max(D[i+1],bk[i]+dis[ring[i]]+maxx);
		maxx=max(maxx,dis[ring[i]]-bk[i]);
	}
	ll ans2=B[cnt];//断开环上首尾(1和cnt) 
	for(int i=1;i<cnt;i++){
		ans2=min( max(max(B[i],D[i+1]) ,A[i]+C[i+1]+val[cnt])  ,  ans2);
		//枚举切断的边 
		//找出最短的直径 
	} 
	printf("%.1lf",(double)(max(ans,ans2))/2.0);
    return 0;
}

11. [NOI2012]迷失游乐园
对于每个点\(i\)
\(f_i\)表示\(i\)第一步往儿子方向走的期望长度,儿子个数记为\(son_i\)
\(g_i\)表示\(i\)第一步往父亲/环上相邻点走的期望长度,父亲/环上相邻点个数记为\(fa_i\)
\(f_i=\frac{\sum_{v\in i\space \space 的儿子} \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space f_v+dis[i][v]}{son_i}\)
\(g_i=\frac{\sum_{v\in i\space \space\space 的父亲/环上相邻的点} \space \space \space\space \space \space\space \space \space\space \space \space\space \space \space\space\space \space \space\space \space \space\space \space \space g_v+dis[i][v]}{fa_i}\)
那么从\(i\)出发的期望路径长度就为\(\frac{f_i*son_i+g_i*fa_i}{son_i+fa_i}\)
考虑转移:
\(i\)不在环上: \(\space x\)\(i\)的父亲:\(g_i=dis[x][i]+\frac{(g_x*fa_x+f_x*son_x-f_i-dis[x][i])}{fa_x+son_x-1}\)
\(i\)在环上:枚举每个环上的点,先计算它顺时针走到环上另外每个点\(x\)的概率\(px\),在这一个点他可以继续顺时针走或者钻到子树里面去,\(g_i+=(\frac{f_v*son_v}{son_v+1}+dis)*px\)\(px=px*\frac{1}{son_x+1}\)。顺时针的最后一个点只能钻到子树里面 \(g_i += (f_x+dis) * px\)。然后再算逆时针就可以了。因此顺时针和逆时针\(px\)初始化都为\(\frac{1}{2}\)
把环上求完了再去求子树里面的up即可
最后答案\(ans=\frac{1}{n}*\sum_{i=1}^n \frac{f_i*son_i+g_i*fa_i}{son_i+fa_i}\)
很多细节,见代码

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=1e7+101;
const int MOD=1e9+7;
const ll inf=2147383647;
const double eps=1e-12;
ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int n,m;
int tot,head[maxn],nx[maxn],to[maxn];
double w[maxn];
void add(int x,int y,int z){
	to[++tot]=y;nx[tot]=head[x];head[x]=tot;w[tot]=z;
} 

int dfn[maxn],top,ff[maxn],get_w[maxn];
int cnt,ring[maxn],is_ring[maxn];
double ring_w[maxn];
void get_ring(int x){
	dfn[x]=++top;
	for(int i=head[x];i;i=nx[i]){
		int v=to[i];if(v==ff[x])continue;
		if(!dfn[v]){
			ff[v]=x;get_w[v]=w[i];
			get_ring(v);
		}
		else if(dfn[v]>dfn[x]){
			for(;v!=x;v=ff[v]){
				ring[++cnt]=v;
				ring_w[cnt]=get_w[v];
				is_ring[v]=1;
			}
			ring[++cnt]=x;
			ring_w[cnt]=w[i];
			is_ring[x]=1;
			return ;
		}
	} 
	return ;
}
double f[maxn],g[maxn],son[maxn],fa[maxn];
void dfs_get_f(int x,int ff){
	for(int i=head[x];i;i=nx[i]){
		int v=to[i];if(v==ff || is_ring[v])continue;
		dfs_get_f(v,x);son[x]+=1.0;
		f[x]+=f[v]+(double)w[i];
	}
	if(!is_ring[x])fa[x]=1.0;
	else fa[x]=2.0;
	if(son[x])f[x]/=son[x];
	return ;
}

void dfs_get_g(int x,int ff){
	for(int i=head[x];i;i=nx[i]){
		int v=to[i];if(v==ff || is_ring[v])continue;
		//注意除以0 
		if(fa[x]+son[x]-1)g[v]=w[i]+(g[x]*fa[x]+f[x]*son[x]-f[v]-w[i])/(fa[x]+son[x]-1.0);
		else g[v]=w[i];
		dfs_get_g(v,x);
	} 
	return ;
}

int main(){
	n=read();m=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read(),z=read();
		add(x,y,z);add(y,x,z);
	}
	get_ring(1);
	for(int i=1;i<=cnt;i++)dfs_get_f(ring[i],ring[i]);
	if(cnt==0)ring[++cnt]=1,dfs_get_f(1,1),fa[1]=0;
	//顺时针 
	for(int i=1;i<cnt;i++)ring[i+cnt]=ring[i],ring_w[i+cnt]=ring_w[i];
	for(int i=1;i<=cnt;i++){
		double px=0.5;
		int u=ring[i];
		for(int j=i;j<i+cnt-1;j++){
			int v=ring[j+1];
			if(j+1!=i+cnt-1)g[u]+=(f[v]*son[v]/(son[v]+1.0)+ring_w[j])*px;
			else g[u]+=(f[v]+ring_w[j])*px;
			px=px/(son[v]+1.0); 
		}
	}
	//逆时针 
	reverse(ring+1,ring+cnt+1);reverse(ring_w+1,ring_w+cnt+1);
	for(int i=1;i<cnt;i++)ring[i+cnt]=ring[i],ring_w[i+cnt]=ring_w[i];
	for(int i=1;i<=cnt;i++){
		double px=0.5;
		int u=ring[i];
		for(int j=i+1;j<=i+cnt-1;j++){
			int v=ring[j];
			if(j!=i+cnt-1)g[u]+=(f[v]*son[v]/(son[v]+1.0)+ring_w[j])*px;
			else g[u]+=(f[v]+ring_w[j])*px;
			px=px/(son[v]+1.0); 
		}
	}
	for(int i=1;i<=cnt;i++)dfs_get_g(ring[i],ring[i]);
	double ans=0;
	for(int i=1;i<=n;i++){ 
		ans=ans+(f[i]*son[i]+g[i]*fa[i])/(son[i]+fa[i]);
	}
	ans/=(double)n;
	printf("%.5lf",ans);
    return 0;
}

12.The Great Wall II
单调栈优化
题解

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=8000+101;
const int MOD=1e9+7;
const ll inf=2147383647;
const double eps=1e-12;
ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int n,a[maxn],dp[maxn][maxn];
//dp[i][j]前i个数分成j段的最小值 
struct info{int a,dp,pre_min;};
int main(){
	memset(dp,0x3f,sizeof(dp));dp[0][1]=0;
	n=read();for(int i=1;i<=n;i++)a[i]=read(),dp[i][1]=max(a[i],dp[i-1][1]);
	cout<<dp[n][1]<<endl;
	for(int j=2;j<=n;j++){
		stack<info>st; 
		for(int i=j;i<=n;i++){
			int dpp=dp[i-1][j-1],dp_minn=inf;
			while(!st.empty() && st.top().a<=a[i]){
				dpp=min(dpp,st.top().dp);
				st.pop();
			}
			if(!st.empty())dp_minn=st.top().pre_min;
			dp[i][j]=min(dp_minn,dpp+a[i]);
			st.push({a[i],dpp,dp[i][j]});
		}
		cout<<dp[n][j]<<endl;
	}  
    return 0;
}
posted @ 2022-08-11 23:09  I_N_V  阅读(41)  评论(0)    收藏  举报