DP的一些杂题(思维型)

来源:互联网 发布:mac双系统 文件 编辑:程序博客网 时间:2024/04/27 22:53

本帖收集收集一些考思维的DP问题啦。


1.P2876 [USACO07JAN]解决问题Problem Solving
题目链接:https://www.luogu.org/problemnew/show/2876
题目大意:有一些任务需要用几个月按顺序完成,每个任务有两个代价ai,bi,其中ai会在完成当月付出,bi会在完成后一个月付出。每个月的可用费用是固定的M(当月的不能在下个月用),问最多要多久可以完成所有任务并且付完款(注意第一个月要赚钱不能开始完成任务)。
题目解法:
f[ i ][ j ]表示完成第 i 个任务,并且最后一次完成了 j 个任务的最短天数。
转移有两种:
第一种:f[ i ][ j ] = f[ i-j ][ k ] + 1; ( Sigma(a[i-j+1],a[i])+Sigma(b[i-j-k+1],b[i-j]) <= M , j!=0) ;
第二种:f[ i ][ 0 ] = f[ i ][ j ] + 1; (Sigma(b[i-j+1],b[i])<=M) ;
其中第一种为在当前月完成新的任务,第二种为不完成新的,只还上一个月的欠款。
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<cstring>#define maxn 305#define RG registerusing namespace std;long long Ans,M,P,f[3*maxn][maxn],a[maxn],b[maxn];int main(){    cin>>M>>P;     memset(f,127,sizeof(f));    f[1][1] = 1;  f[0][0] = 0;  f[1][0]=2;                           for(RG int i = 1; i <= P; i ++)cin>>a[i]>>b[i];    for(RG int i = 1; i <= P; i ++)a[i]+=a[i-1],b[i]+=b[i-1];    for(RG int i = 2; i <= P; i ++){        for(RG int j = 1; j <= i; j ++)            for(RG int k = 0; k <= i-j; k ++)                      //这个月买并且还上次的债                if( (a[i]-a[i-j]) + (b[i-j]-b[i-j-k]) <= M)                    f[i][j] = min(f[i][j],f[i-j][k] + 1);        for(RG int j = 1; j <= P; j ++)                            //这个月不买只还款            if(b[i] - b[i-j]<=M)f[i][0] = min(f[i][0],f[i][j]+1);     }    Ans = f[P][0]+1;                                                //+1是因为第一个月要赚钱,没得花    for(RG int i = 1; i <= P; i ++)        if(b[P]-b[i-1]<=M)Ans = min(Ans,f[P][i]+2);                 //还要还钱...    cout<<Ans;  return 0;}

2.P3177 [HAOI2015]树上染色
题目链接:https://www.luogu.org/problemnew/show/P3177
题目大意:有一棵点数为 N 的树,树边有边权。给你一个在 0~ N 之内的正整数 K ,你要在这棵树中选择* K个点,将其染成黑色,并将其他 的N-K个点染成白色 。 将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和*的受益。问受益最大值是多少。
题目解法:
f[ u ][ j ]表示u的子树中选择 j 个黑点的最优解。
每遍历一个儿子即可后,枚举儿子内选多少个黑点,计算这些黑点与外面其他黑点的贡献和(距离为边长),类似树上背包。
贡献的计算(u到儿子v):(v内黑点数 * v外黑点数 * 边长)+(v内白点数 * v外白点数 * 边长)
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<cstring>#include<algorithm>#include<cmath>#define IL inline#define RG register#define maxn 2005using namespace std;long long f[maxn][maxn],siz[maxn],N,M,K,x,y,z;struct Road{long long to,next,lg;}t[2*maxn]; long long head[maxn],cnt;void Dfs(RG int u,RG int fth){    siz[u] = 1;    for(RG int i = head[u];i;i = t[i].next){        int v = t[i].to; if(v == fth)continue;        Dfs(v,u);         ll len = t[i].lg;        for(RG int j = siz[u]; j >= 0; j --)            for(RG int k = min(K-j,siz[v]); k >= 0; k --)                f[u][j+k] = max(f[u][j+k],                                f[v][k] + f[u][j] + 1ll*len*k*(K-k) + 1ll*((N-K)-(siz[v]-k))*(siz[v]-k)*len);        siz[u] = siz[u] + siz[v];    }return;}int main(){    cin>>N>>K;    for(RG int i = 1; i <= N-1; i ++){        cin>>x>>y>>z;        t[++cnt] = (Road){x,head[y],z};  head[y] = cnt;        t[++cnt] = (Road){y,head[x],z};  head[x] = cnt;    }       Dfs(1,0); cout<<f[1][K];    return 0;} 

这里写图片描述
数据范围:n,Q<=100000,s<=10;
题目链接:无(这是某次考试的神题啦)
题目大意:咳咳咳,上面够清楚了吧。
题目解法:
f[ i ][ j ] 表示 i 的子树里选择大小为 j 的联通块方案之和。
那么显然有: f[ u ][ i+j ] = f[ u ][ i+j ] + f[ u ][ i ]*f[ v ][ j ];
然后注意到 s <=10特别的小,i、j的枚举只用枚举到smax = 10即可。
本题关键在于上面这个DP式子是可以逆过来的:f[ u ][ i+j ] = f[ u ][ i+j ] - f[ u ][ i ]*f[ v ][ j ];
A.修改操作:从下向上先消去对应儿子的贡献,然后从下到上再次DP一遍。
B.查询操作:容斥原理从上往下DP,每次用DP到的当前祖先消去对应儿子后的DP值来更新其对应儿子的值,用两个数组转接即可。
由于数据随机,高度为log级别,所以查询与修改都为log级别
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long long#define RG register#define mxs 10#define maxn 100005#define mod 1000000007using namespace std;ll N,Q,s,k,c,top,tras,nw;ll fa[maxn],val[maxn],stk[maxn],f[maxn][mxs+2];IL ll qkPow(RG ll bs,RG ll js){    ll S=1,T=bs;while(js){    if(js&1)S*=T,S%=mod; T*=T;T%=mod; js>>=1;}return S;}IL void Merge(RG int u,RG int v){    for(RG int i = mxs; i >= 1; i --)        for(RG int j = 1; j <= i-1; j ++)            f[u][i] = (f[u][i] + 1ll*f[u][j]*f[v][i-j])%mod;    return;}IL void Calc(RG ll &s){if(s<0)s= s+((-s)/mod+1)*mod; if(s>=mod)s=s%mod;}IL void Seperate(RG int u,RG int v){    for(RG int i = 1; i <= mxs; i ++)        for(RG int j = 1; j <= i-1; j ++)            f[u][i] = f[u][i] - 1ll*f[u][j]*f[v][i-j],Calc(f[u][i]);    return;}IL void Solve_Modify(){                               //修改    scanf("%lld %lld",&k,&c); top = 0;    for(RG int i = k;i;i = fa[i])stk[++top] = i;    for(RG int i = top-1; i >= 1; i --)Seperate(stk[i+1],stk[i]);    ll nw = 1ll*qkPow(val[k],mod-2)*c%mod; val[k] = c;    for(RG int i = 1; i <= mxs ; i ++)f[k][i] *= 1ll*nw,f[k][i]%=mod;    for(RG int i = 1; i <= top-1; i ++)Merge(stk[i+1],stk[i]);}IL void Solve_GetAns(){                               //查询    scanf("%lld %lld",&k,&s); top = 0;    for(RG int i = k;i;i = fa[i])stk[++top] = i;    for(RG int i = 0; i <= mxs; i ++)f[0][i] = 0;    while(stk[top] && top){        nw = stk[top--]; tras = stk[top];        for(RG int i = 1; i <= mxs; i ++)f[N+1][i] = f[0][i];          //f[N+1]为中途转运数组        for(RG int i = 1; i <= mxs; i ++)f[0][i] = f[nw][i];           //f[0]记录当前的答案        Merge(0,N+1);        if(top)Seperate(0,tras);     }printf("%lld\n",f[0][s]%mod);}int main(){    freopen("tree.in","r",stdin);    freopen("tree.out","w",stdout);    cin>>N>>Q;    for(RG int i = 1; i <= N ;i ++)scanf("%lld",&val[i]),f[i][1] = val[i];    for(RG int i = 2; i <= N ;i ++)scanf("%lld",&fa[i]);    for(RG int i = N; i > 1; i --)Merge(fa[i],i);    while(Q--){        int op;scanf("%d",&op);        if(op == 0)Solve_Modify();    if(op == 1)Solve_GetAns();    }return 0;}

4.P2331 [SCOI2005]最大子矩阵
题目链接:https://www.luogu.org/problemnew/show/P2331
题目大意:这里有一个n*m的矩阵,请你选出其中k个子矩阵,使得这个k个子矩阵分值之和最大。注意:选出的k个子矩阵不能相互重叠(可以为空矩阵!)(m<=2)。
题目解法:
对于m=1与m=2分情况讨论。
对于m=1就是来搞笑的,f[ i ][ j ]表示到i行选了j个,转移方程:f[i][j]=max( f[v][j-1] + Sigam(v+1,i) );
对于m=2,设f[ i ][ j ][ k ]表示第一列处理到 i ,第二列处理到 j,已经选择了k个矩阵 ,类似于最长公共子序列,总共三种转移。
- 1.选第一列的1*H矩阵: f[i][j][k] = max{ f[t][j][k-1] + Sigma1(t+1,i) };
- 2.选第二列的1*H矩阵:f[i][j][k] = max{ f[i][t][k-1] + Sigma1(t+1,j) };
- 3.选两列并行的2*H矩阵:f[i][j][k]=max{ f[t][t][k-1]+Sigma1(t+1,min(i,j))+Sigma2(t+1,min(i,j)) };
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long longusing namespace std;ll g[1005][200],Ans,a[1005][5],f[505][505][40],N,M,K;IL void Solve1(){    for(ll i = 1; i <= N ; i ++)a[i][0] = gi() + a[i-1][0];    for(ll i = 1; i <= N ; i ++)        for(ll j = 1; j <= min(i,K); j ++)            for(ll t = 0; t <= i-1 ; t ++){                g[i][j] = max(g[i][j],g[t][j-1]+a[i][0]-a[i-1][0]);                g[i][j] = max(g[i][j],g[t][j]+a[i][0]-a[t][0]);            }    Ans = 0;    for(ll i = 1; i <= N ; i ++)for(ll j = 1; j <= min(i,K); j ++) Ans = max(Ans , g[i][j]);    cout<<Ans;}IL void Solve2(){    for(ll i = 1; i <= N ; i ++)for(ll j = 0; j <= 1 ; j ++)a[i][j] = gi();    for(ll i = 1; i <= N ; i ++)a[i][2] = a[i][0] + a[i][1];    for(ll i = 1; i <= N ; i ++){        a[i][0] += a[i-1][0];  a[i][1] += a[i-1][1];        a[i][2] += a[i-1][2];    }    for(ll i = 1; i <= N ; i ++)        for(ll j = 1; j <= N ; j ++){            for(ll k = 1; k <= min(i+j,K); k ++){                for(ll t = 0; t <= i-1; t ++)                    f[i][j][k] = max(f[i][j][k],max(f[t][j][k],f[t][j][k-1]+a[i][0]-a[t][0]));                for(ll t = 0; t <= j-1; t ++)                    f[i][j][k] = max(f[i][j][k],max(f[i][t][k],f[i][t][k-1]+a[j][1]-a[t][1]));                for(ll t = 0; t <= min(i,j)-1; t ++)                    f[i][j][k] = max(f[i][j][k],max(f[t][t][k],f[t-1][t-1][k-1]+a[min(i,j)][2]-a[t][2]));            }        }    for(ll i = 0; i <= K; i ++)Ans = max(Ans,f[N][N][i]);    cout<<Ans;  return;}int main(){    N = gi(); M = gi(); K = gi();      //1<= M <=2 !!!!!!!    if(M == 1)Solve1();    else Solve2();  return 0;}

5.P2051 [AHOI2009]中国象棋
题目链接:https://www.luogu.org/problemnew/show/P2051
题目大意:在一个N行M列的棋盘上,让你放若干个炮(可以是0个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。(N,M<=100)
题目解法:
f[ i ][ j ][ k ] 表示当前处理到第 i ,有 j 放置了1个炮,有 k 放置了2个炮的方案总数。
转移有几种:
本行不放
本行放一个(放在有1个的列上或有0个的列上),
本行放两个(两个在有1个的列上,两个在有0个的列上,一个在0个上一个在1个上)。
分类讨论然后转移即可,详细链接:https://www.luogu.org/wiki/show?name=%E9%A2%98%E8%A7%A3+P2051
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#define RG register#define ll long long#define mod 9999973using namespace std;ll f[105][105][105],N,M,Ans;inline ll Calc(RG int bs){return (bs*(bs-1)/2)%mod;}   //从bs个中选择2个的方案数int main(){    freopen("testdate.in","r",stdin);    cin>>N>>M;    f[0][0][0] = 1;    for(RG ll i = 1; i <= N; ++ i){        for(RG ll j = 0; j <= M; ++ j){            for(RG ll k = 0; k <= M-j; ++ k){                f[i][j][k] = ( f[i][j][k] + f[i-1][j][k] ) %mod;                if(j-1>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j-1][k]*(M-k-(j-1)) ) %mod;                if(k-1>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j+1][k-1]*(j+1) ) %mod;                if(j-2>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j-2][k]*Calc( M-(j-2)-k ) ) %mod;                if(k-1>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j][k-1]*j*(M-j-(k-1)) ) %mod;                if(k-2>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j+2][k-2]*Calc(j+2) ) %mod;    }}}    Ans = 0;    for(RG ll j = 0; j <= M; ++ j)        for(RG ll k = 0; k <= M; ++ k)            Ans = (Ans + f[N][j][k]) %mod;    cout<<Ans; return 0;}

6. P2501 - 【DP合集】矩阵 matrix(CJOJ)
题目链接:http://oj.changjun.com.cn/problem/detail/pid/2501
题目大意:给出一个 n × m 的矩阵。请在其中选择至多 3 个互不相交的,大小恰为 k × k 的子矩阵,使得子矩阵的 权值和最大。(N、M<=1500)
题目解法:
本题题解矩阵对应端点为其右下方端点,S[ i ][ j ]即为其面积。
非常巧妙的思路,
A.
f[ i ][ j ]表示选择( i , j )这个点对应矩阵,并且还选择1~0个矩阵的最优解。
upmx表示 当前点可转移的上方所有点 对应值S最优值。 lfmx[ j ]表示前j列中的最优矩阵S
那么有f[ i ][ j ] = max( upmx , lfmx[ j- K ] ) + S[ i ][ j ];
B.
最巧妙的地方,我们最多可以选择3个矩阵,那么是不是可以看做选一个f[ t1 ][ t2 ]加上一个S[ i ][ j ]
upmx表示 当前点可转移的上方所有点 对应值f最优值。 lfmx[ j ]表示前j列中的最优矩阵f
那么有ans[ i ][ j ] = max( upmx , lfmx[ j- K] ) + S[ i ][ j ];
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long long#define RG register#define maxn 1503using namespace std;int N,M,K,Ans,f[maxn][maxn];int a[maxn][maxn],s[maxn][maxn],lfmx[maxn],upmx;IL void Matrix(){    for(RG int i =1 ; i <= N ; ++ i)for(RG int j = 1; j <= M ; ++ j)scanf("%d",&a[i][j]);    for(RG int i = 1; i <= N ; ++ i)        for(RG int j = 1; j <= M ; ++ j)            a[i][j] = a[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];    for(RG int i = K; i <= N ; ++ i)        for(RG int j = K; j <= M ; ++ j)            s[i][j] = a[i][j] + a[i-K][j-K] - a[i-K][j] - a[i][j-K];    return;}IL void Solve(){    upmx = 0; memset(lfmx,0,sizeof(lfmx));    for(RG int i = K; i <= N; ++ i){        for(RG int j = K; j <= M; ++ j)upmx = max(upmx,s[i-K][j]);        for(RG int j = K; j <= M; ++ j){            lfmx[j] = max(lfmx[j-1],lfmx[j]);            lfmx[j] = max(lfmx[j],s[i][j]);            f[i][j] = max(upmx,lfmx[j-K]) + s[i][j];        }    }    upmx = 0; memset(lfmx,0,sizeof(lfmx));    for(RG int i = K; i <= N; ++ i){        for(RG int j = K; j <= M; ++ j)upmx = max(upmx,f[i-K][j]);        for(RG int j = K; j <= M; ++ j){            lfmx[j] = max(lfmx[j-1],lfmx[j]);            lfmx[j] = max(lfmx[j],f[i][j]);            Ans = max(Ans , max(upmx,lfmx[j-K])+s[i][j]);        }    }return;}int main(){    cin>>N>>M>>K;  Matrix();    Ans = 0; Solve();    cout<<Ans; return 0;}

7.打比赛
这里写图片描述
输入格式:第一行一个N,之后N行每行四个:ai,bi,ci,di;
两人都是大佬不会出现提交错误(都一遍AC),询问罚时较长的那个人的罚时最小值。(N,a,b,c,d<=500)
题目解法:
显然先都自己切题,结束后再一次性完成教学操作是最优解(无影响)。
那么问题转化为:每一道题至少要有一个人切掉,剩下的开黑互助,问最短罚时。
f[ i ][ j ]表示当前处理到第 i 题,GodCowC花费 j 时间时 FoolMike的最短费时。
显然对于每一道题,有三种决策方式,分别转移即可。
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long long#define maxn 505#define RG register#define INF 1e16using namespace std;ll SigmaA,SigmaB,Sigma,N,Ans,a[maxn],b[maxn],c[maxn],d[maxn],f[maxn][maxn*maxn];int main(){    freopen("atcoder.in","r",stdin);    freopen("atcoder.out","w",stdout);    cin>>N;  Sigma = SigmaA = SigmaA = 0;    for(RG int i = 1; i <= N; i ++)        scanf("%lld %lld %lld %lld",&a[i],&b[i],&c[i],&d[i]);    for(RG int i = 1; i <= N; i ++)SigmaA = SigmaA + a[i];    for(RG int i = 1; i <= N; i ++)SigmaB = SigmaB + b[i];    Sigma = max(SigmaA,SigmaB);    for(RG int i = 0; i <= N; i ++)        for(RG int j = 0; j <= Sigma; j ++)f[i][j] = INF;    f[0][0] = 0;    for(RG int i = 1; i <= N; i ++){        for(RG int j = 0; j <= Sigma; j ++){            if(j-a[i]>=0)f[i][j] = min(f[i][j],f[i-1][j-a[i]]+b[i]);            if(j-a[i]-c[i]>=0)f[i][j] = min(f[i][j],f[i-1][j-a[i]-c[i]]+c[i]);            if(j-d[i]>=0)f[i][j] = min(f[i][j],f[i-1][j-d[i]]+b[i]+d[i]);        }    } Ans = INF;    for(RG ll j = 0; j <= Sigma; j ++)Ans = min(Ans,max(f[N][j],j));    cout<<Ans;    fclose(stdin);  fclose(stdout);    return 0;}

8.P2747 [USACO5.4]周游加拿大Canada Tour
题目链接:https://www.luogu.org/problemnew/show/P2747
题目大意:在一个有N个节点的图上找出两条不相交的路径,使得两条路径上不同节点数总和最大。
题目解法:
f[ i ][ j ]‘表示其中一条路径到了 i ,另一条路径到了 j 时的最多城市数。
转移时强制 i < j,f[ i ][ j ] = f[ j ][ i ] = max{ f[ i ][ k ]+1 }; ( f[ i ][ k ]>0 && dis[ k ][ j ]=true )
由于除了起点外,只有从f[ t ][ t ]转移时路径才会相交,但这种转移并不存在,所以这个DP方程是正确的。
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#include<map>#define IL inline#define ll long long#define RG register#define maxn 105using namespace std;map<string,int>cd;  string s,s1,s2;bool dis[maxn][maxn]; int f[maxn][maxn],N,V,Ans;int main(){    freopen("testdate.in","r",stdin);    cin>>N>>V;    for(RG int i = 1; i <= N; i ++){cin>>s; cd[s] = i;}    for(RG int i = 1; i <= V; i ++){cin>>s1>>s2; dis[cd[s1]][cd[s2]]=dis[cd[s2]][cd[s1]]=1;}    f[1][1] = 1;    for(RG int i = 1; i <= N; i ++){        for(RG int j = i+1; j <= N; j ++){            for(RG int k = 1; k < j; k ++)                if(dis[j][k] && f[i][k])f[i][j] = f[j][i] = max(f[i][j],f[i][k]+1);        }    }Ans = 1;    for(RG int i = 1; i <= N; i ++)if(dis[i][N])Ans = max(Ans,f[N][i]);    cout<<Ans;  return 0;}

8.CJOJ P2455 - 【51Nod】遥远的旅行
题目链接:http://oj.changjun.com.cn/problem/detail/pid/2455
题目大意:在一张N个结点M条边,有边权的图上,询问是否存在一种走法,满足起点为1,终点为N,并且路径总长为T。
(T<=1e18 ,单条边权<=1e4,N<=50,M<=50)
题目解法:
乍一看没有什么思路,仔细思考一下单条边权范围的作用。
我们假设存在一条1~N的路径为 S , 走了这条路径后反复走一条与N号结点相连的长度为 w 的边,可以实现题目所需。
那么( T - S ) == 2k * w是肯定的,转化一下:( T - S ) % ( 2*w ) == 0
所以T%(2*w) == S%*(2*w),这下子距离范围就直接缩减到单条比边权的范围内了。
我们枚举与N相接的每条边,长度为 w ,设f[ i ][ j ]表示从 1 出发到 i ,满足 路径长度 % ( 2*w ) == j 的最短路。
带入SPFA跑就行了,最后只用检查 f[ N ][ T%(2*w) ] <= T即可(满足条件的最短路径是否在T范围以内)。
注意本题的T范围特别大,初始赋 INF 时最大值要赋为 1e18+1ek (K>=2)。
参考题解:http://blog.csdn.net/crybymyself/article/details/54974562
实现代码:

 #include<cstdio>#include<cstdlib>#include<algorithm>#include<cstring>#include<queue>  #define IL inline#define maxn 55#define mxdis 10005#define ll long long#define RG register#define INF 1e18+1e3using namespace std;ll Case,N,M,T,head[maxn],cnt;  ll dis[maxn][2*mxdis]; bool vis[maxn][2*mxdis];struct Node{int x,k;}; struct Road{ll to,next,lg;}t[2*maxn];queue<Node>Q;IL void Addedge(){    ll uu,vv,ww;  scanf("%lld%lld%lld",&uu,&vv,&ww); uu++; vv++;    t[++cnt] = (Road){vv,head[uu],ww}; head[uu]=cnt;    t[++cnt] = (Road){uu,head[vv],ww}; head[vv]=cnt;}IL void DP(RG ll md){                       //其实本质上就是一个SPFA啦    for(RG int i = 1;i <= N; i ++)     for(RG int j = 0; j <= md; j ++){vis[i][j]=false;dis[i][j]=INF;}    while(!Q.empty())Q.pop();    Q.push((Node){1,0}); vis[1][0]=true; dis[1][0]=0;    while(!Q.empty()){        Node u = Q.front(); Q.pop();    for(RG int i = head[u.x];i;i = t[i].next){            int v = t[i].to,k = (u.k+t[i].lg)%md;         if(dis[v][k] > dis[u.x][u.k] + t[i].lg){            dis[v][k] = dis[u.x][u.k] + t[i].lg;        if(!vis[v][k]){Q.push((Node){v,k}); vis[v][k] = true;}        }    }vis[u.x][u.k] = false;    }return;}IL void Work(){    scanf("%lld%lld%lld\n",&N,&M,&T);    for(RG int i = 1; i <= N; i ++)head[i]=0; cnt = 0;    for(RG int i = 1; i <= M; i ++)Addedge();    for(RG int i = head[N];i;i = t[i].next){        ll v = t[i].to,w = 2*t[i].lg;  DP(w);    if(dis[N][T%w] <= T){puts("Possible");return;}    }puts("Impossible");}int main(){    freopen("testdate.in","r",stdin);    cin>>Case; while(Case--)Work();  return 0;}

9.P1417 烹调方案
题目链接:https://www.luogu.org/problemnew/show/1417
题目大意:每种菜有三个参数ai , bi , ci 。做第 i 个菜要用 ci 时间,第 i 个菜在 第 t 时刻完成,对答案的贡献为 ai - t*bi,询问在时间限制 T 中可以获得的最大贡献。
题目解法:
先讨论一下将什么性质的菜放在前面更优。假设有A、B两个菜。
顺序为AB,贡献为:(A.a)-(A.c)(A.b)+(B.a)-(A.c+B.c) B.b;同理可以写出顺序为BA的。
两边消去,化简就可得到:AB优于BA的条件为:A.c/A.b < B.x/B.b,数学归纳法可以将这个结论推到多个。
所以先按照刚刚推演的优先级排序,然后DP即可。
DP方程不难: f[ i ][ j ]表示 处理到第 i 个菜,用了 j 时间的最优值。
f[ i ][ j ] = f[ i-1 ][ j - Itm[ i ].c ] + Itm[ i ].a - j * Itm[ i ].b;暴力跑就行了。
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define RG register#define ll long long#define maxn 200005using namespace std;ll f[maxn],T,N,Ans;struct Item{ll a,b,c; }itm[maxn];inline bool cmp1(Item A,Item B){return A.c*B.b < B.c*A.b;}  // (A.c/A.b) < (B.c/B.b);int main(){    cin>>T>>N;    for(RG int i = 1; i <= N; i ++)cin>>itm[i].a;    for(RG int i = 1; i <= N; i ++)cin>>itm[i].b;    for(RG int i = 1; i <= N; i ++)cin>>itm[i].c;    sort(itm+1,itm+N+1,cmp1);    for(RG int i = 1; i <= N; i ++)    for(RG int j = T; j >= itm[i].c; j --)        f[j] = max(f[j],f[j-itm[i].c]+itm[i].a-1ll*j*itm[i].b);    Ans = 0; for(RG int i = 1; i <= T; i ++)Ans = max(Ans,f[i]);    cout<<Ans;  return 0;}

本帖收集收集一些考思维的DP问题啦。


1.P2876 [USACO07JAN]解决问题Problem Solving
题目链接:https://www.luogu.org/problemnew/show/2876
题目大意:有一些任务需要用几个月按顺序完成,每个任务有两个代价ai,bi,其中ai会在完成当月付出,bi会在完成后一个月付出。每个月的可用费用是固定的M(当月的不能在下个月用),问最多要多久可以完成所有任务并且付完款(注意第一个月要赚钱不能开始完成任务)。
题目解法:
f[ i ][ j ]表示完成第 i 个任务,并且最后一次完成了 j 个任务的最短天数。
转移有两种:
第一种:f[ i ][ j ] = f[ i-j ][ k ] + 1; ( Sigma(a[i-j+1],a[i])+Sigma(b[i-j-k+1],b[i-j]) <= M , j!=0) ;
第二种:f[ i ][ 0 ] = f[ i ][ j ] + 1; (Sigma(b[i-j+1],b[i])<=M) ;
其中第一种为在当前月完成新的任务,第二种为不完成新的,只还上一个月的欠款。
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<cstring>#define maxn 305#define RG registerusing namespace std;long long Ans,M,P,f[3*maxn][maxn],a[maxn],b[maxn];int main(){    cin>>M>>P;     memset(f,127,sizeof(f));    f[1][1] = 1;  f[0][0] = 0;  f[1][0]=2;                           for(RG int i = 1; i <= P; i ++)cin>>a[i]>>b[i];    for(RG int i = 1; i <= P; i ++)a[i]+=a[i-1],b[i]+=b[i-1];    for(RG int i = 2; i <= P; i ++){        for(RG int j = 1; j <= i; j ++)            for(RG int k = 0; k <= i-j; k ++)                      //这个月买并且还上次的债                if( (a[i]-a[i-j]) + (b[i-j]-b[i-j-k]) <= M)                    f[i][j] = min(f[i][j],f[i-j][k] + 1);        for(RG int j = 1; j <= P; j ++)                            //这个月不买只还款            if(b[i] - b[i-j]<=M)f[i][0] = min(f[i][0],f[i][j]+1);     }    Ans = f[P][0]+1;                                                //+1是因为第一个月要赚钱,没得花    for(RG int i = 1; i <= P; i ++)        if(b[P]-b[i-P]<=M)Ans = min(Ans,f[P][i]+2);                 //还要还钱...    cout<<Ans;  return 0;}

2.P3177 [HAOI2015]树上染色
题目链接:https://www.luogu.org/problemnew/show/P3177
题目大意:有一棵点数为 N 的树,树边有边权。给你一个在 0~ N 之内的正整数 K ,你要在这棵树中选择* K个点,将其染成黑色,并将其他 的N-K个点染成白色 。 将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和*的受益。问受益最大值是多少。
题目解法:
f[ u ][ j ]表示u的子树中选择 j 个黑点的最优解。
每遍历一个儿子即可后,枚举儿子内选多少个黑点,计算这些黑点与外面其他黑点的贡献和(距离为边长),类似树上背包。
贡献的计算(u到儿子v):(v内黑点数 * v外黑点数 * 边长)+(v内白点数 * v外白点数 * 边长)
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<cstring>#include<algorithm>#include<cmath>#define IL inline#define RG register#define maxn 2005using namespace std;long long f[maxn][maxn],siz[maxn],N,M,K,x,y,z;struct Road{long long to,next,lg;}t[2*maxn]; long long head[maxn],cnt;void Dfs(RG int u,RG int fth){    siz[u] = 1;    for(RG int i = head[u];i;i = t[i].next){        int v = t[i].to; if(v == fth)continue;        Dfs(v,u);         ll len = t[i].lg;        for(RG int j = siz[u]; j >= 0; j --)            for(RG int k = min(K-j,siz[v]); k >= 0; k --)                f[u][j+k] = max(f[u][j+k],                                f[v][k] + f[u][j] + 1ll*len*k*(K-k) + 1ll*((N-K)-(siz[v]-k))*(siz[v]-k)*len);        siz[u] = siz[u] + siz[v];    }return;}int main(){    cin>>N>>K;    for(RG int i = 1; i <= N-1; i ++){        cin>>x>>y>>z;        t[++cnt] = (Road){x,head[y],z};  head[y] = cnt;        t[++cnt] = (Road){y,head[x],z};  head[x] = cnt;    }       Dfs(1,0); cout<<f[1][K];    return 0;} 

这里写图片描述
数据范围:n,Q<=100000,s<=10;
题目链接:无(这是某次考试的神题啦)
题目大意:咳咳咳,上面够清楚了吧。
题目解法:
f[ i ][ j ] 表示 i 的子树里选择大小为 j 的联通块方案之和。
那么显然有: f[ u ][ i+j ] = f[ u ][ i+j ] + f[ u ][ i ]*f[ v ][ j ];
然后注意到 s <=10特别的小,i、j的枚举只用枚举到smax = 10即可。
本题关键在于上面这个DP式子是可以逆过来的:f[ u ][ i+j ] = f[ u ][ i+j ] - f[ u ][ i ]*f[ v ][ j ];
A.修改操作:从下向上先消去对应儿子的贡献,然后从下到上再次DP一遍。
B.查询操作:容斥原理从上往下DP,每次用DP到的当前祖先消去对应儿子后的DP值来更新其对应儿子的值,用两个数组转接即可。
由于数据随机,高度为log级别,所以查询与修改都为log级别
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long long#define RG register#define mxs 10#define maxn 100005#define mod 1000000007using namespace std;ll N,Q,s,k,c,top,tras,nw;ll fa[maxn],val[maxn],stk[maxn],f[maxn][mxs+2];IL ll qkPow(RG ll bs,RG ll js){    ll S=1,T=bs;while(js){    if(js&1)S*=T,S%=mod; T*=T;T%=mod; js>>=1;}return S;}IL void Merge(RG int u,RG int v){    for(RG int i = mxs; i >= 1; i --)        for(RG int j = 1; j <= i-1; j ++)            f[u][i] = (f[u][i] + 1ll*f[u][j]*f[v][i-j])%mod;    return;}IL void Calc(RG ll &s){if(s<0)s= s+((-s)/mod+1)*mod; if(s>=mod)s=s%mod;}IL void Seperate(RG int u,RG int v){    for(RG int i = 1; i <= mxs; i ++)        for(RG int j = 1; j <= i-1; j ++)            f[u][i] = f[u][i] - 1ll*f[u][j]*f[v][i-j],Calc(f[u][i]);    return;}IL void Solve_Modify(){                               //修改    scanf("%lld %lld",&k,&c); top = 0;    for(RG int i = k;i;i = fa[i])stk[++top] = i;    for(RG int i = top-1; i >= 1; i --)Seperate(stk[i+1],stk[i]);    ll nw = 1ll*qkPow(val[k],mod-2)*c%mod; val[k] = c;    for(RG int i = 1; i <= mxs ; i ++)f[k][i] *= 1ll*nw,f[k][i]%=mod;    for(RG int i = 1; i <= top-1; i ++)Merge(stk[i+1],stk[i]);}IL void Solve_GetAns(){                               //查询    scanf("%lld %lld",&k,&s); top = 0;    for(RG int i = k;i;i = fa[i])stk[++top] = i;    for(RG int i = 0; i <= mxs; i ++)f[0][i] = 0;    while(stk[top] && top){        nw = stk[top--]; tras = stk[top];        for(RG int i = 1; i <= mxs; i ++)f[N+1][i] = f[0][i];          //f[N+1]为中途转运数组        for(RG int i = 1; i <= mxs; i ++)f[0][i] = f[nw][i];           //f[0]记录当前的答案        Merge(0,N+1);        if(top)Seperate(0,tras);     }printf("%lld\n",f[0][s]%mod);}int main(){    freopen("tree.in","r",stdin);    freopen("tree.out","w",stdout);    cin>>N>>Q;    for(RG int i = 1; i <= N ;i ++)scanf("%lld",&val[i]),f[i][1] = val[i];    for(RG int i = 2; i <= N ;i ++)scanf("%lld",&fa[i]);    for(RG int i = N; i > 1; i --)Merge(fa[i],i);    while(Q--){        int op;scanf("%d",&op);        if(op == 0)Solve_Modify();    if(op == 1)Solve_GetAns();    }return 0;}

4.P2331 [SCOI2005]最大子矩阵
题目链接:https://www.luogu.org/problemnew/show/P2331
题目大意:这里有一个n*m的矩阵,请你选出其中k个子矩阵,使得这个k个子矩阵分值之和最大。注意:选出的k个子矩阵不能相互重叠(可以为空矩阵!)(m<=2)。
题目解法:
对于m=1与m=2分情况讨论。
对于m=1就是来搞笑的,f[ i ][ j ]表示到i行选了j个,转移方程:f[i][j]=max( f[v][j-1] + Sigam(v+1,i) );
对于m=2,设f[ i ][ j ][ k ]表示第一列处理到 i ,第二列处理到 j,已经选择了k个矩阵 ,类似于最长公共子序列,总共三种转移。
- 1.选第一列的1*H矩阵: f[i][j][k] = max{ f[t][j][k-1] + Sigma1(t+1,i) };
- 2.选第二列的1*H矩阵:f[i][j][k] = max{ f[i][t][k-1] + Sigma1(t+1,j) };
- 3.选两列并行的2*H矩阵:f[i][j][k]=max{ f[t][t][k-1]+Sigma1(t+1,min(i,j))+Sigma2(t+1,min(i,j)) };
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long longusing namespace std;ll g[1005][200],Ans,a[1005][5],f[505][505][40],N,M,K;IL void Solve1(){    for(ll i = 1; i <= N ; i ++)a[i][0] = gi() + a[i-1][0];    for(ll i = 1; i <= N ; i ++)        for(ll j = 1; j <= min(i,K); j ++)            for(ll t = 0; t <= i-1 ; t ++){                g[i][j] = max(g[i][j],g[t][j-1]+a[i][0]-a[i-1][0]);                g[i][j] = max(g[i][j],g[t][j]+a[i][0]-a[t][0]);            }    Ans = 0;    for(ll i = 1; i <= N ; i ++)for(ll j = 1; j <= min(i,K); j ++) Ans = max(Ans , g[i][j]);    cout<<Ans;}IL void Solve2(){    for(ll i = 1; i <= N ; i ++)for(ll j = 0; j <= 1 ; j ++)a[i][j] = gi();    for(ll i = 1; i <= N ; i ++)a[i][2] = a[i][0] + a[i][1];    for(ll i = 1; i <= N ; i ++){        a[i][0] += a[i-1][0];  a[i][1] += a[i-1][1];        a[i][2] += a[i-1][2];    }    for(ll i = 1; i <= N ; i ++)        for(ll j = 1; j <= N ; j ++){            for(ll k = 1; k <= min(i+j,K); k ++){                for(ll t = 0; t <= i-1; t ++)                    f[i][j][k] = max(f[i][j][k],max(f[t][j][k],f[t][j][k-1]+a[i][0]-a[t][0]));                for(ll t = 0; t <= j-1; t ++)                    f[i][j][k] = max(f[i][j][k],max(f[i][t][k],f[i][t][k-1]+a[j][1]-a[t][1]));                for(ll t = 0; t <= min(i,j)-1; t ++)                    f[i][j][k] = max(f[i][j][k],max(f[t][t][k],f[t-1][t-1][k-1]+a[min(i,j)][2]-a[t][2]));            }        }    for(ll i = 0; i <= K; i ++)Ans = max(Ans,f[N][N][i]);    cout<<Ans;  return;}int main(){    N = gi(); M = gi(); K = gi();      //1<= M <=2 !!!!!!!    if(M == 1)Solve1();    else Solve2();  return 0;}

5.P2051 [AHOI2009]中国象棋
题目链接:https://www.luogu.org/problemnew/show/P2051
题目大意:在一个N行M列的棋盘上,让你放若干个炮(可以是0个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。(N,M<=100)
题目解法:
f[ i ][ j ][ k ] 表示当前处理到第 i ,有 j 放置了1个炮,有 k 放置了2个炮的方案总数。
转移有几种:
本行不放
本行放一个(放在有1个的列上或有0个的列上),
本行放两个(两个在有1个的列上,两个在有0个的列上,一个在0个上一个在1个上)。
分类讨论然后转移即可,详细链接:https://www.luogu.org/wiki/show?name=%E9%A2%98%E8%A7%A3+P2051
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#define RG register#define ll long long#define mod 9999973using namespace std;ll f[105][105][105],N,M,Ans;inline ll Calc(RG int bs){return (bs*(bs-1)/2)%mod;}   //从bs个中选择2个的方案数int main(){    freopen("testdate.in","r",stdin);    cin>>N>>M;    f[0][0][0] = 1;    for(RG ll i = 1; i <= N; ++ i){        for(RG ll j = 0; j <= M; ++ j){            for(RG ll k = 0; k <= M-j; ++ k){                f[i][j][k] = ( f[i][j][k] + f[i-1][j][k] ) %mod;                if(j-1>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j-1][k]*(M-k-(j-1)) ) %mod;                if(k-1>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j+1][k-1]*(j+1) ) %mod;                if(j-2>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j-2][k]*Calc( M-(j-2)-k ) ) %mod;                if(k-1>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j][k-1]*j*(M-j-(k-1)) ) %mod;                if(k-2>=0)f[i][j][k] = ( f[i][j][k] + f[i-1][j+2][k-2]*Calc(j+2) ) %mod;    }}}    Ans = 0;    for(RG ll j = 0; j <= M; ++ j)        for(RG ll k = 0; k <= M; ++ k)            Ans = (Ans + f[N][j][k]) %mod;    cout<<Ans; return 0;}

6. P2501 - 【DP合集】矩阵 matrix(CJOJ)
题目链接:http://oj.changjun.com.cn/problem/detail/pid/2501
题目大意:给出一个 n × m 的矩阵。请在其中选择至多 3 个互不相交的,大小恰为 k × k 的子矩阵,使得子矩阵的 权值和最大。(N、M<=1500)
题目解法:
本题题解矩阵对应端点为其右下方端点,S[ i ][ j ]即为其面积。
非常巧妙的思路,
A.
f[ i ][ j ]表示选择( i , j )这个点对应矩阵,并且还选择1~0个矩阵的最优解。
upmx表示 当前点可转移的上方所有点 对应值S最优值。 lfmx[ j ]表示前j列中的最优矩阵S
那么有f[ i ][ j ] = max( upmx , lfmx[ j- K ] ) + S[ i ][ j ];
B.
最巧妙的地方,我们最多可以选择3个矩阵,那么是不是可以看做选一个f[ t1 ][ t2 ]加上一个S[ i ][ j ]
upmx表示 当前点可转移的上方所有点 对应值f最优值。 lfmx[ j ]表示前j列中的最优矩阵f
那么有ans[ i ][ j ] = max( upmx , lfmx[ j- K] ) + S[ i ][ j ];
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long long#define RG register#define maxn 1503using namespace std;int N,M,K,Ans,f[maxn][maxn];int a[maxn][maxn],s[maxn][maxn],lfmx[maxn],upmx;IL void Matrix(){    for(RG int i =1 ; i <= N ; ++ i)for(RG int j = 1; j <= M ; ++ j)scanf("%d",&a[i][j]);    for(RG int i = 1; i <= N ; ++ i)        for(RG int j = 1; j <= M ; ++ j)            a[i][j] = a[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];    for(RG int i = K; i <= N ; ++ i)        for(RG int j = K; j <= M ; ++ j)            s[i][j] = a[i][j] + a[i-K][j-K] - a[i-K][j] - a[i][j-K];    return;}IL void Solve(){    upmx = 0; memset(lfmx,0,sizeof(lfmx));    for(RG int i = K; i <= N; ++ i){        for(RG int j = K; j <= M; ++ j)upmx = max(upmx,s[i-K][j]);        for(RG int j = K; j <= M; ++ j){            lfmx[j] = max(lfmx[j-1],lfmx[j]);            lfmx[j] = max(lfmx[j],s[i][j]);            f[i][j] = max(upmx,lfmx[j-K]) + s[i][j];        }    }    upmx = 0; memset(lfmx,0,sizeof(lfmx));    for(RG int i = K; i <= N; ++ i){        for(RG int j = K; j <= M; ++ j)upmx = max(upmx,f[i-K][j]);        for(RG int j = K; j <= M; ++ j){            lfmx[j] = max(lfmx[j-1],lfmx[j]);            lfmx[j] = max(lfmx[j],f[i][j]);            Ans = max(Ans , max(upmx,lfmx[j-K])+s[i][j]);        }    }return;}int main(){    cin>>N>>M>>K;  Matrix();    Ans = 0; Solve();    cout<<Ans; return 0;}

7.打比赛
这里写图片描述
输入格式:第一行一个N,之后N行每行四个:ai,bi,ci,di;
两人都是大佬不会出现提交错误(都一遍AC),询问罚时较长的那个人的罚时最小值。(N,a,b,c,d<=500)
题目解法:
显然先都自己切题,结束后再一次性完成教学操作是最优解(无影响)。
那么问题转化为:每一道题至少要有一个人切掉,剩下的开黑互助,问最短罚时。
f[ i ][ j ]表示当前处理到第 i 题,GodCowC花费 j 时间时 FoolMike的最短费时。
显然对于每一道题,有三种决策方式,分别转移即可。
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long long#define maxn 505#define RG register#define INF 1e16using namespace std;ll SigmaA,SigmaB,Sigma,N,Ans,a[maxn],b[maxn],c[maxn],d[maxn],f[maxn][maxn*maxn];int main(){    freopen("atcoder.in","r",stdin);    freopen("atcoder.out","w",stdout);    cin>>N;  Sigma = SigmaA = SigmaA = 0;    for(RG int i = 1; i <= N; i ++)        scanf("%lld %lld %lld %lld",&a[i],&b[i],&c[i],&d[i]);    for(RG int i = 1; i <= N; i ++)SigmaA = SigmaA + a[i];    for(RG int i = 1; i <= N; i ++)SigmaB = SigmaB + b[i];    Sigma = max(SigmaA,SigmaB);    for(RG int i = 0; i <= N; i ++)        for(RG int j = 0; j <= Sigma; j ++)f[i][j] = INF;    f[0][0] = 0;    for(RG int i = 1; i <= N; i ++){        for(RG int j = 0; j <= Sigma; j ++){            if(j-a[i]>=0)f[i][j] = min(f[i][j],f[i-1][j-a[i]]+b[i]);            if(j-a[i]-c[i]>=0)f[i][j] = min(f[i][j],f[i-1][j-a[i]-c[i]]+c[i]);            if(j-d[i]>=0)f[i][j] = min(f[i][j],f[i-1][j-d[i]]+b[i]+d[i]);        }    } Ans = INF;    for(RG ll j = 0; j <= Sigma; j ++)Ans = min(Ans,max(f[N][j],j));    cout<<Ans;    fclose(stdin);  fclose(stdout);    return 0;}

8.P2747 [USACO5.4]周游加拿大Canada Tour
题目链接:https://www.luogu.org/problemnew/show/P2747
题目大意:在一个有N个节点的图上找出两条不相交的路径,使得两条路径上不同节点数总和最大。
题目解法:
f[ i ][ j ]‘表示其中一条路径到了 i ,另一条路径到了 j 时的最多城市数。
转移时强制 i < j,f[ i ][ j ] = f[ j ][ i ] = max{ f[ i ][ k ]+1 }; ( f[ i ][ k ]>0 && dis[ k ][ j ]=true )
由于除了起点外,只有从f[ t ][ t ]转移时路径才会相交,但这种转移并不存在,所以这个DP方程是正确的。
实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#include<map>#define IL inline#define ll long long#define RG register#define maxn 105using namespace std;map<string,int>cd;  string s,s1,s2;bool dis[maxn][maxn]; int f[maxn][maxn],N,V,Ans;int main(){    freopen("testdate.in","r",stdin);    cin>>N>>V;    for(RG int i = 1; i <= N; i ++){cin>>s; cd[s] = i;}    for(RG int i = 1; i <= V; i ++){cin>>s1>>s2; dis[cd[s1]][cd[s2]]=dis[cd[s2]][cd[s1]]=1;}    f[1][1] = 1;    for(RG int i = 1; i <= N; i ++){        for(RG int j = i+1; j <= N; j ++){            for(RG int k = 1; k < j; k ++)                if(dis[j][k] && f[i][k])f[i][j] = f[j][i] = max(f[i][j],f[i][k]+1);        }    }Ans = 1;    for(RG int i = 1; i <= N; i ++)if(dis[i][N])Ans = max(Ans,f[N][i]);    cout<<Ans;  return 0;}

9.CJOJ P2455 - 【51Nod】遥远的旅行
题目链接:http://oj.changjun.com.cn/problem/detail/pid/2455
题目大意:在一张N个结点M条边,有边权的图上,询问是否存在一种走法,满足起点为1,终点为N,并且路径总长为T。
(T<=1e18 ,单条边权<=1e4,N<=50,M<=50)
题目解法:
乍一看没有什么思路,仔细思考一下单条边权范围的作用。
我们假设存在一条1~N的路径为 S , 走了这条路径后反复走一条与N号结点相连的长度为 w 的边,可以实现题目所需。
那么( T - S ) == 2k * w是肯定的,转化一下:( T - S ) % ( 2*w ) == 0
所以T%(2*w) == S%*(2*w),这下子距离范围就直接缩减到单条比边权的范围内了。
我们枚举与N相接的每条边,长度为 w ,设f[ i ][ j ]表示从 1 出发到 i ,满足 路径长度 % ( 2*w ) == j 的最短路。
带入SPFA跑就行了,最后只用检查 f[ N ][ T%(2*w) ] <= T即可(满足条件的最短路径是否在T范围以内)。
注意本题的T范围特别大,初始赋 INF 时最大值要赋为 1e18+1ek (k>=2)。
参考题解:http://blog.csdn.net/crybymyself/article/details/54974562
实现代码:

 #include<cstdio>#include<cstdlib>#include<algorithm>#include<cstring>#include<queue>  #define IL inline#define maxn 55#define mxdis 10005#define ll long long#define RG register#define INF 1e18+1e3using namespace std;ll Case,N,M,T,head[maxn],cnt;  ll dis[maxn][2*mxdis]; bool vis[maxn][2*mxdis];struct Node{int x,k;}; struct Road{ll to,next,lg;}t[2*maxn];queue<Node>Q;IL void Addedge(){    ll uu,vv,ww;  scanf("%lld%lld%lld",&uu,&vv,&ww); uu++; vv++;    t[++cnt] = (Road){vv,head[uu],ww}; head[uu]=cnt;    t[++cnt] = (Road){uu,head[vv],ww}; head[vv]=cnt;}IL void DP(RG ll md){                       //其实本质上就是一个SPFA啦    for(RG int i = 1;i <= N; i ++)     for(RG int j = 0; j <= md; j ++){vis[i][j]=false;dis[i][j]=INF;}    while(!Q.empty())Q.pop();    Q.push((Node){1,0}); vis[1][0]=true; dis[1][0]=0;    while(!Q.empty()){        Node u = Q.front(); Q.pop();    for(RG int i = head[u.x];i;i = t[i].next){            int v = t[i].to,k = (u.k+t[i].lg)%md;         if(dis[v][k] > dis[u.x][u.k] + t[i].lg){            dis[v][k] = dis[u.x][u.k] + t[i].lg;        if(!vis[v][k]){Q.push((Node){v,k}); vis[v][k] = true;}        }    }vis[u.x][u.k] = false;    }return;}IL void Work(){    scanf("%lld%lld%lld\n",&N,&M,&T);    for(RG int i = 1; i <= N; i ++)head[i]=0; cnt = 0;    for(RG int i = 1; i <= M; i ++)Addedge();    for(RG int i = head[N];i;i = t[i].next){        ll v = t[i].to,w = 2*t[i].lg;  DP(w);    if(dis[N][T%w] <= T){puts("Possible");return;}    }puts("Impossible");}int main(){    freopen("testdate.in","r",stdin);    cin>>Case; while(Case--)Work();  return 0;}

10.P2594 - 【JZOJ5230】队伍统计
题目链接:http://oj.changjun.com.cn/problem/detail/pid/2594
题目大意:有一些前后矛盾关系,求解一个1~N的排列,最多不能违背k条矛盾关系(即不能有超过k条矛盾关系(u,v),满足最后v排在了u前面) ( N、K <= 20 )。
题目解法:
观察到N、K <= 20,可以状态压缩 2^20 大小。
f[ i ][ j ]表示 违背了 i 个关系,当前入队的人的集合为 j 的方案数。
显然可以预处理出每个人对应的矛盾关系def[ x ],加入一个人,新增矛盾关系即为 ( j & def[ x ] )中 1 的个数,这个也是可以预处理出来的。
想到状压内容转移还是很容易的f[ i+sum[def[x]&j] ][ j|(1<< i) ] += f[ i ][ j ];
实现代码:

//#pragma GCC optimize (2)        // 本题时限应该为 1.5 sec,而且卡常数,卡常数卡到死也只能1.06秒跑出来(这里只能开O2过原时限啦)#include<bits/stdc++.h>#define IL inline#define mod 1000000007#define RG register#define maxn 21#define lmit 1048576using namespace std;IL int gi(){    RG int date = 0; RG char ch = 0;    while(ch<'0'||ch>'9')ch = getchar();    while('0'<=ch && ch<='9'){date = date * 10 + ch - '0';ch = getchar();} return date;}int N,M,K,def[maxn],sum[lmit],f[maxn][lmit],top[maxn];IL void GetInit(){    N = gi();M = gi();K = gi(); top[0]=1;  RG int u,v;    for(RG int i = 1; i <= N; ++ i)top[i] = top[i-1]<<1;    for(RG int i = 1; i <= M; ++ i){        u = gi()-1; v = gi()-1;     def[u] |= top[v] ;    }    for(RG int i = 0; i < top[N]; ++ i){        RG int cnt = 0,tp = i; while(tp){tp-=(tp&-tp); ++cnt;}    sum[i] = cnt;    }}IL void Solve(){    f[0][0] = 1;        for(RG int k = 0; k <= K; ++ k)                        //已经有了k个矛盾    for(RG int sf = 0; sf < top[N]; ++ sf){                      if(f[k][sf]){             for(RG int i = 0; i < N; ++ i){             //当前这个人是谁            if(!(sf & top[i])){            if( sum[sf & def[i]] + k <= K ){                   f[ k + sum[def[i]&sf] ][ sf|top[i] ] += f[ k ][ sf ];                   if(f[ k + sum[def[i]&sf] ][ sf|top[i] ]>=mod)f[ k+sum[def[i]&sf] ][ sf|top[i] ]-=mod;   }}}}} }int main(){    freopen("testdate.in","r",stdin);    GetInit();  Solve();  RG long long Ans = 0;    for(RG int i = 0; i <= K; ++ i)Ans += f[ i ][ top[N]-1 ];    printf("%lld",Ans%mod);  return 0;}

11.洛谷比赛 — U14959 模拟城市2.0(大火题)
题目链接:https://www.luogu.org/problemnew/show/U14959
题目描述:
开发区的建筑地块是一个n×n的矩形,而开发区可以建造三种建筑: 商业楼,住宅楼,教学楼。这任何两座建筑可以堆叠,可以紧密相邻。他需要建造正好a座商业楼,b座住宅楼,c座教学楼。但是,城市建成后要应付检查,如果安排的太混乱会被批评。不过幸运的是,只有一条公路经过了该开发区的一侧,就是说,检察人员全程只能看到开发区的一面。
因此,他需要使得开发区建成后,从正面看去,只有一种类型的建筑。
一共有多少种满足条件的方案呢? 请输出方案数,并对1e9+7取模。
注意,对于同一个n,会有多组数据。
数据范围:(N,a,b,c )<= 25 ;( T )<= 5 * 10^5

输入输出格式
输入格式:
第一行两个整数n,T
接下来T行,每行三个整数,表示该组数据的a,b,c。
输出格式:
输出共T行,每行一个整数:表示各数据答案取模1e9+7的结果。

样例以及样例解释
这里写图片描述
样例答案中的8种情况如下:
这里写图片描述

题目解法:
其实只有两种方块:可以看到的,不能看到的。

A.
首先每一列之间相互都是没有影响的,考虑单列的方案数。
f[ i ][ j ][ k ][ x ][ y ]表示到了第 i 行,当前这一行高度为 j ,整个的最高高度为 k ,用了 x 个可以看见的,用了 y 个看不见的。
考虑一下,转移应该有两种:放到下一行,或者向上叠一层。转移:
//放到下一行:f[ i ][ j ][ k ][ x ][ y ] ⇒ f[ i+1 ][ 0 ][ k ][ x ][ y ]
//向上叠一层:
//————( j == k ):f[ i ][ j ][ k ][ x ][ y ] ⇒f[ i ][ j+1 ][ k+1 ][ x+1 ][ y ]
//————( j < k ):f[ i ][ j ][ k ][ x ][ y ]⇒ f[ i ][ j+1 ][ k ][ x+1 ][ y ] and f[ i ][ j+1 ][ k ][ x ][ y+1 ]
显然有:j <= k , x >= k ,k <=max(a,b,c)=mx; 答案最终都存到了f [ N+1 ][ 0 ][ k ][ x ][ y ]里了。

那么令 way[ x ][ y ]表示这一列用 x 个可以看见的,y 个看不见的 的总方案数。
整理一下即可: way[ x ][ y ] = Sigma( f[N+1][ 0 ][ k ][ x ][ y ]) <0<=k<=mx>

B.
一列的way处理出来了,考虑处理N列的。
g[ i ][ x ][ y ]表示处理到第 i 列,已经用了 x 个可以看见的, y 个看不见的的方案数。
那么转移还是非常简单的:
g[ i ][ x ][ y ] * way[ t1 ][ t2 ] ⇒ g[ i+1][ x+t1 ][ y+t2 ]
然后为了方便计算(为了好看),令ans[ x ][ y ]表示在 N*N 的地图中,选择 x 个可以看见的 , y 个看不见的方案数。
显然 ans[ x ][ y ] = g[ N ][ x ][ y ]

C.
观察到我们的T组数据的N是不变的。 所以我们可以直接预处理出mx = 25的ans[ x ][ y ],然后对每次询问O( 1 )回答即可。
考虑以 a 为可以看见的,b、c为看不见的的方案数。( 另外两种类似 )
首先固定的方案书为 ans[ a ][ b+c ],那么对于看不见的,一共有 b+c 个位置,我们只需要任意排列即可。
显然排列方式用组合数算一共有 C[ b ][ b+c ] == C[ c ][ b+c ]。预处理一下组合数C。
那么对于每次询问,选a为看见的方案数为 C[ b ][ b+c ] * ans[ a ][ b+c ]
同理处理一下以 b、c为可看见的情况,累加起来统计答案即可。

D.
本题的确还是非常难的。 首先观察到 N<= 25,要想到 N^5 的复杂度是可以满足的。
然后本题最难的地方在于 f 数组的求解,关键在于想到还要设置一个 k 来记录最高高度,将影响因素加入DP维数中。
后面的部分难度其实不是特别大,组合数那里要有转化的思想。
很考察思维的一道DP大火题,对代码能力也有一定要求(细节太多)。

实现代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define IL inline#define ll long long#define mod 1000000007#define RG register#define Mx 30using namespace std;IL int gi(){    int date = 0,m = 1; char ch = 0;    while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();    if(ch == '-'){m = -1 ; ch = getchar();}    while('0'<=ch && ch<='9'){date = date * 10 + ch - '0';ch = getchar();}    return date * m;}int C[2*Mx][2*Mx],T,N,Ans;int f[Mx][Mx][Mx][Mx][2*Mx],way[Mx][2*Mx],g[Mx][Mx][2*Mx],ans[Mx][2*Mx];IL void Comber(){    C[0][0] = 1; int mx = 50;    for(RG int i = 1; i <= mx; i ++)C[0][i] = 1;    for(RG int i = 1; i <= mx; i ++)    for(RG int j = 1; j <= i; j ++)C[j][i] = (C[j-1][i-1] + C[j][i-1])%mod;    return;}IL void Plus(RG int & bs,RG int pls){bs = bs + pls; if(bs>=mod)bs-=mod;}  //bs = (bs+pls)%modIL void Solve1(){    int mx = 25;    f[1][0][0][0][0] = 1;    for(RG int i = 1; i <= N; i ++){    for(RG int j = 0; j <= mx; j ++)        for(RG int k = j; k <= mx; k ++)        for(RG int x = k; x <= mx; x ++)            for(RG int y = 0; y <= 2*mx; y ++)                if(f[i][j][k][x][y]){                Plus( f[i+1][0][k][x][y] , f[i][j][k][x][y] );                if(j == k)Plus( f[i][j+1][k+1][x+1][y] , f[i][j][k][x][y] );                else{                    Plus( f[i][j+1][k][x+1][y] , f[i][j][k][x][y] );                Plus( f[i][j+1][k][x][y+1] , f[i][j][k][x][y] );                }            }     }    for(RG int k = 0; k <= mx; k ++)       for(RG int x = 0; x <= mx; x ++)          for(RG int y = 0; y <= 2*mx; y ++)          Plus( way[x][y] , f[N+1][0][k][x][y] );    return;}IL void Solve2(){    g[0][0][0] = 1; int mx = 25;    for(RG int i = 1; i <= N; i ++)    for(RG int x = 0; x <= mx; x ++)        for(RG int y = 0; y <= 2*mx; y ++)        for(RG int t1 = 0;t1 <= x; t1 ++)            for(RG int t2 = 0; t2 <= y; t2 ++)            Plus( g[i][x][y] , 1ll*g[i-1][x-t1][y-t2]*way[t1][t2]%mod );    for(RG int x = 0; x <= mx; x ++)    for(RG int y = 0; y <= 2*mx; y ++)        ans[x][y] = g[N][x][y];    return;}IL void Work(){    int a = gi(),b = gi(),c = gi(); Ans = 0;    Plus(Ans , 1ll*ans[a][b+c]*C[b][b+c]%mod );    Plus(Ans , 1ll*ans[b][a+c]*C[c][a+c]%mod );    Plus(Ans , 1ll*ans[c][a+b]*C[a][a+b]%mod );    printf("%d\n",Ans);  return;}int main(){    freopen("testdate.in","r",stdin);    N = gi(); T = gi();    Comber(); Solve1(); Solve2();    while(T--)Work(); return 0; }