本帖收集收集一些考思维的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;
}

DP的一些杂题(思维型)相关推荐

  1. AcWing蓝桥杯AB组辅导课10、疑难杂题

    文章目录 前言 例题1:AcWing 1242. 修改数组(并查集) 分析 题解:单链表式并查集 例题2:AcWing 1234. 倍数问题(背包问题+贪心) 分析 题解1:01背包问题,三维解法(贪 ...

  2. 杂题记录及简要题解(一)

    一些前几天做过的还不错的但是不是太想专门花一整篇博客的篇幅去写的题就简要地记录在这里. 说是简要题解,其实写得还是挺详细的.之后的杂题记录可能就会写得简略一点. CF1060E Sergey and ...

  3. 20190509杂题选讲

    这次杂题选讲好多思维题神仙题啊= =顺便学了波线段树上二分= = Normal 题目大意戳这 CF1083C CDW讲的神仙题*1 题解戳这 AGC002E 我讲的题,是个人写的程序都比我写的程序跑得 ...

  4. 动态规划dp(带模板题の超易懂版):01背包,完全背包,分组背包,多重背包,混合背包

    动态规划dp(带模板题の超易懂版):01背包,完全背包,分组背包,多重背包 01背包 && 完全背包 && 分组背包 の 视频教程:https://www.bilibi ...

  5. ACM图论+数据结构杂题总结

    ACM:图论+数据结构杂题总结 T1: 题目描述:(出处:Atcoder Regular Contest 067 Yakiniku Restaurants) 一条街上有N家烧烤餐馆.餐厅从西到东编号为 ...

  6. ACM杂题——动态规划_背包问题

    ACM杂题K - I NEED A OFFER!--动态规划_背包问题优化解法 题目描述 Speakless很早就想出国,现在他已经考完了所有需要的考试,准备了所有要准备的材料,于是,便需要去申请学校 ...

  7. Algorithm(基础+提高+Top+杂题)

    写在前面的话 此篇涵盖:经典题目544道左右+杂题,每天都会更新一些题目和详细的题解,另外还有一些自己平时遇到的难题,给自己打个笔记,供以后复习使用,同时希望对大家有所帮助,感谢支持~. level1 ...

  8. 【做题记录】DP 杂题

    P2577 [ZJOI2004]午餐 $\texttt{solution}$ 想到贪心: 吃饭慢的先打饭节约时间, 所以先将人按吃饭时间从大到小排序. 状态: \(f[i][j]\) 表示前 \(i\ ...

  9. 【杂题集】单题小总结

    单题小总结 2017.7.16 开始埋坑~ -> Vijos 渡河:块染色:缩点建图并更新距离,也可在floodfill同时刷新(这个效率高). -> Vijos 摇钱树:排序让损失单调化 ...

最新文章

  1. Python创建virtualenv(虚拟环境)方法
  2. linux下配置ip地址四种方法(图文)
  3. 职业生涯中12个愚蠢想法
  4. httpclient异步发送请求_关于Tornado5.1:到底是真实的异步和还是虚假的异步
  5. 认清js中var a=b=1和var a=1,b=1的区别
  6. 计算机c盘d盘不显示了怎么办,我的电脑打开里头的C盘D盘的图标怎么显示不了啊......
  7. asp.net MVC2 初探十一
  8. 适合小白了解学习的DevOps实践
  9. 模拟——扫雷游戏(洛谷 P2670)
  10. python如何互换_python中怎么交换列的顺序
  11. MyBatis-Plus条件查询——Wrapper
  12. 程序员如何年薪百万?深度学习必读书籍!
  13. 新概念英语(1-29)Come in, Amy.
  14. requestLayout() improperly called by
  15. Java的三种代理模式【附源码分析】
  16. matlab矩阵旋转
  17. 朱有鹏老师linux核心大讲堂---ARM裸机第十一部分 NandFlash和iNand学习笔记
  18. 网站首页的设计(转)
  19. linux perl脚本介绍(初学者)
  20. 网管用计算机上岗证书,普通高中校园网计算机教室建设标准

热门文章

  1. 【附源码】计算机毕业设计java综合众筹网站设计与实现
  2. 陪玩MM千千万,谁是你的NO.1?使用Python获取陪玩照片进行颜值检测打分
  3. 免费生成!火爆全网的个人行程卡纪念版!
  4. 《ZigBee开发笔记》第五部分 外设篇 - 基础实验 第2章 CC2530温湿度传感器DHT11
  5. 创建Mesh-格子地图转NavMesh-可破坏墙壁
  6. 计算机教室布置软木,软木照片墙布置,让孩子体验手工的乐趣
  7. 【Django】Django 的员工信息系统
  8. java内置功能的使用,装箱拆箱及枚举类
  9. 2021年IT行业现状及就业前景怎样?
  10. 上市公司股利分红数据(1991-2020)