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; }
- DP的一些杂题(思维型)
- 思维型DP
- 能够锻炼思维的一些急转弯题!!!
- 一些解决问题的思维
- 【DP】最近做的一些DP题
- 一些不良的思维习惯!!!
- 一些不良的思维习惯!!!
- 一些不良的思维习惯!!
- 一些思维性的东西
- 学习java的一些思维!
- 01背包详解,DP思维的转换
- hdu1158(很好的一道dp思维练习)
- 充满思维含量的DP 选美
- CERC 2014 L题 区间dp 思维
- 【CF】【318div2D】【思维题】【DP|数学】
- hdu 4472 Count【思维+dp】好题
- 一些不良的管理思维习惯!!!
- 一些不良的管理思维习惯!!!
- object_getClass与self.class的区别
- 洛谷P1250 种树(差分约束)
- java中的反射机制-1(学习总结)
- 解决 IB算法错误
- Python学习---4
- DP的一些杂题(思维型)
- linux下配置多个tomcat
- mssql server 各种链接问题
- 洛谷 1462 通往奥格瑞玛的道路
- 【Java】类集之Collection
- 蓝桥杯 递归 放苹果
- 纯虚函数和抽象类简介
- PHP开发API接口及使用
- 怎样用java写一个简单的文件复制程序