DP的一些杂题(思维型)

本帖收集收集一些考思维的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 register
using 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 2005
using 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 1000000007
using 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 long
using 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 9999973
using 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 1503
using 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 1e16
using 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 105
using 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+1e3
using 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 200005
using 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 register
using 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 2005
using 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 1000000007
using 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 long
using 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 9999973
using 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 1503
using 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 1e16
using 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 105
using 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+1e3
using 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 1048576
using 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 30
using 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)%mod
IL 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; 
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值