这里学习一下DP的正确姿势。
也为了ZJOI2019去水一下做一些准备
题解就随便写写啦.
后续还是会有专题练习和综合练习的.
P1005 矩阵取数游戏
给出$n \times m$矩阵每次在每一行取n个数,一共取m次,
第i次取数的权值是$2^i$,给出一个取数的顺序,最大化取完所有数的贡献和。
输出贡献和。
对于100%的数据$1\leq n,m \leq 80,0
需要使用高精度,考虑一个DP,$f[i][j][k]$表示第i行,共取j次,其中k次在前面,显然(j-k)在后面
第j次取数在前面取的那么$f[i][j][k]$是由$f[i][j-1][k-1]$转移而来,得dp方程$f[i][j][k]=f[i][j-1][k-1]+2^j \times a[k]$ (取的数在从左往右数第$k$个)
第j次取数在后面取的那么$f[i][j][k]$是由$f[i][j-1][k]$转移而来,得dp方程$f[i][j-1][k]+2^j \times a[m-(j-k)+1]$ (取的数在从左向右数第$m-(j-k)+1$个)
然后整理可得DP方程$f[i][j][k]=max{f[i][j-1][k-1]+2^j \times a[k] , f[i][j-1][k]+2^j \times a[m-(j-k)+1] }$
考虑边界条件对于$f[i][j][k]$是有意义的当且仅当$0 \leq j \leq k$,然后使用高精度计算(预处理2的幂的Lint数)
复杂度$O(km^3)$,其中k是高精度数的位数可视为常数
code:
# include# define fp(i,s,t) for(int i=s;i<=t;i++) # define int long long using namespace std; const int N=85; const int L=50; char s[L]; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } struct Lint{ int len,num[L]; Lint () { len=0; memset(num,0,sizeof(num));} Lint change(int x) { Lint a; if (x==0) { a.num[1]=0;a.len=1; return a;} a.len=0; while (x>0) a.num[++a.len]=x%10,x/=10; return a; } void print(Lint x) { for (int i=x.len;i>=1;i--) putchar(x.num[i]+'0'); putchar('\n'); } Lint operator + (const Lint &t) const { Lint ans; memset(ans.num,0,sizeof(ans.num)); ans.len=max(len,t.len); for (int i=1;i<=ans.len;i++) { ans.num[i]+=num[i]+t.num[i]; ans.num[i+1]+=ans.num[i]/10; ans.num[i]%=10; } if (ans.num[ans.len+1]>0) ans.len++; return ans; } Lint operator * (const Lint &t) const { Lint ans; for (int i=1;i<=len;i++) for (int j=1;j<=t.len;j++) ans.num[i+j-1]+=num[i]*t.num[j]; for (int i=1;i<=len+t.len;i++) { ans.num[i+1]+=ans.num[i]/10; ans.num[i]%=10; } if (ans.num[len+t.len]>0) ans.len=len+t.len; else ans.len=len+t.len-1; return ans; } bool operator < (const Lint &t) const { if (len>t.len) return false; else if (len return true; for (int i=len;i>=1;i--) if (num[i] return true; else if (num[i]>t.num[i]) return false; return false; } }rd; Lint Max(Lint x,Lint y){if (x return y; else return x;} int n,m; Lint f[N][N],a[N][N],ans; Lint pw[N]; signed main() { scanf("%lld%lld",&n,&m); fp(i,1,n) fp(j,1,m) a[i][j]=rd.change(read()); pw[0]=rd.change(1); Lint num_2=rd.change(2); fp(i,1,m) pw[i]=pw[i-1]*num_2; fp(i,1,n) { Lint ret=rd.change(0); memset(f,0,sizeof(f)); fp(j,1,m) fp(k,0,j) { if (k!=0) f[j][k]=f[j-1][k-1]+pw[j]*a[i][k]; if (j-1>=k-1) f[j][k]=max(f[j][k],f[j-1][k]+pw[j]*a[i][m-j+k+1]); } fp(k,0,m) ret=Max(ret,f[m][k]); ans=ans+ret; } rd.print(ans); return 0; }
P1026 统计单词个数
给出一个长度为m的字符串,和n个字典$S_i$ ,分成k份,每份里面单独处理,若一个位置为单词首字母可以在后匹配出一个字典,
那么有1的贡献,求出一个分配方案使得字符串分成k份后,贡献值最大。
对于100%的数据$1 \leq m,n \leq 200, 1 \leq k \leq 40 , 字典S_i\leq 6$
如果我们知道s的唯一连续划分[l,r]中总贡献是GetVal(l,r),那么就是一个简单的动态规划问题了。
令$f[i][j]$表示前i个字母划分成j份,最大贡献,显然是一个位置k转移过来增加GetVal(k+1,i)这么多贡献
即$f[i][j]=max\{ f[k][j-1]+GetVal(k+1,i)\}$,然后考虑GetVal(l,r)的暴力求解,直接枚举每一位i属于[l,r],然后枚举n个字典暴力匹配
注意判断是否超出r的右边界的限制,不要加进去。
然后如果枚举i的时候在线做GetVal会多个n的复杂度,不妨离线$O(n^3)$预处理出$val[l,r]=GetVal(l,r)$
然后$O(n^3)$的动态规划,显然是跑不满的。
# includeusing namespace std; string s,th; char mp[205][205]; int p,k,n,m; int f[205][205],val[205][205]; bool check(int pos,int limt) { for (int i=1;i<=n;i++) { int l=strlen(mp[i]); if (pos+l-1>limt||pos+l-1>m) continue; bool flag=true; for (int j=0;j ) if (s[pos+j]!=mp[i][j]) { flag=false; break; } if (flag) return true; } return false; } int GetVal(int l,int r) { int ret=0; for (int i=l;i<=r;i++) if (check(i,r)) ret++; return ret; } int main() { scanf("%d%d",&p,&k); s="#"; for (int i=1;i<=p;i++) cin>>th,s+=th; scanf("%d",&n); for (int i=1;i<=n;i++) cin>>mp[i]; m=s.length()-1; for (int i=1;i<=m;i++) for (int j=i;j<=m;j++) val[i][j]=GetVal(i,j); for (int i=1;i<=m;i++) for (int j=1;j<=i;j++) { # define k kk for (int k=j-1;k<=i-1;k++) f[i][j]=max(f[i][j],f[k][j-1]+val[k+1][i]); #undef k } printf("%d\n",f[m][k]); return 0; }
P1099 树网的核
bzoj1999 【数据加强版】
给定一棵n个点的带边权无根树,在其直径上求出一段长度不超过s的路径F,使得离路径距离最远的点到路径的距离最短。
求出这个离路径距离最远的点到路径的距离最短的值。
对于100%的数据$1 \leq n \leq 10^5 ,0 \leq w_i \leq 10^3 $
可以证明,路径F存在于任意一条直径上都可以得到答案。
考虑找出一条树的直径,拉成一条链,就是m个点m-1条边的链
二分一个答案MID
然后对于链上每一个点做一个最长链,记为$w_i$然后从链的两端往里跳,如果可以往里跳尽量往里跳(需要使用MID和$w_i$参数)
得到最终的左右区间[Left,Right]显然若区间为空那么一定可以,
若区间不为空,那么扫描[Left,Right]判断其$w_i$是不是超过了$MID$,其(Right-Left)条边权和是不是超过了限制$s$
然后转化为判定类问题。
复杂度$O(max\{N log_2T,n\}) $ 其中T表示最长链长度,N表示最长链节点数,n表示树节点个数。
可以过BZOJ加强版数据
# includeusing namespace std; const int N=500005; int dist[N],d[N],head[N],pre[N],w[N],f[N]; int n,m,s,tot,ans; bool vis[N]; struct rec{ int pre,to,w; }a[N<<1]; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } void adde(int u,int v,int w) { a[++tot].pre=head[u]; a[tot].to=v; a[tot].w=w; head[u]=tot; } void dfs1(int u,int fath,int L) { dist[u]=L; for (int i=head[u];i;i=a[i].pre){ int v=a[i].to; if (v==fath) continue; pre[v]=u; dfs1(v,u,L+a[i].w); } } void dfs2(int u,int fath,int L) { ans=max(ans,L); vis[u]=true; for (int i=head[u];i;i=a[i].pre) { int v=a[i].to; if (vis[v]) continue; dfs2(v,u,L+a[i].w); } } bool check(int MID) { # define lift Lift # define Right Right f[0]=0; int left=d[0],right=1; for (int i=1;i<=d[0];i++) { f[i]=max(f[i-1]+dist[i],w[i]); if (f[i]>MID) {left=i; break;} } f[d[0]+1]=0; for (int i=d[0];i>=1;i--) { f[i]=max(f[i+1]+dist[i-1],w[i]); if (f[i]>MID) { right=i; break;} } if (left>right) return true; int Sum=0; for (int i=left;i<=right;i++) { if (w[i]>MID) return false; if (i!=right) Sum+=dist[i]; } if (Sum<=s) return true; else return false; #undef left #undef right } void GetZhiJin() { dfs1(1,0,0); int p1=1; for (int i=1;i<=n;i++) if (dist[i]>dist[p1]) p1=i; dfs1(p1,0,0); int p2=1; for (int i=1;i<=n;i++) if (dist[i]>dist[p2]) p2=i; swap(p1,p2); while (p1!=p2) { d[++d[0]]=p1; for (int i=head[p1];i;i=a[i].pre){ int v=a[i].to; if (v==pre[p1]) dist[d[0]]=a[i].w; } p1=pre[p1]; } d[++d[0]]=p2; dist[d[0]]=0; } int main() { n=read();s=read(); for (int i=2;i<=n;i++) { int u=read(),v=read(),w=read(); adde(u,v,w),adde(v,u,w); } GetZhiJin(); int L=0,R=0; for (int i=1;i<=d[0];i++) vis[d[i]]=true,R+=dist[i]; for (int i=1;i<=d[0];i++) { ans=0; dfs2(d[i],0,0); w[i]=ans; } while (L<=R) { int mid=(L+R)>>1; if (check(mid)) ans=mid,R=mid-1; else L=mid+1; } cout< '\n'; return 0; }
P1351 联合权值
求一棵n个节点的树,所有边权为$1$,距离为$2$的点对权值乘积最大值、权值乘积和,
其中权值乘积和需要对$10007$取模
对于100%的数据$n\leq 2\times 10^5 , 1 \leq w_i \leq 10^3$
枚举每个点考虑这个点连接到的若干个点,设这个集合为S,那么S重的点互相两两配对都是符合条件的点对。
然后考虑最大值显然是S中两个最大的点权相乘,求和显然是每一个点和前面的点贡献和的2倍,维护即可
复杂度O(kn),其中k表示节点的直接儿子数的平均值,可以看做常数
# include# define int long long using namespace std; const int N=200000+10; const int mo=10007; int val[N],tot,head[N]; struct rec{int pre,to;}a[N<<1]; void adde(int u,int v) { a[++tot].pre=head[u]; a[tot].to=v; head[u]=tot; } signed main() { int n; scanf("%lld",&n); int u,v; for (int i=2;i<=n;i++) { scanf("%lld%lld",&u,&v); adde(u,v); adde(v,u); } for (int i=1;i<=n;i++) scanf("%lld",&val[i]); int ans1=0,ans2=0; for (int i=1;i<=n;i++) { int u=i; int ret=0; int last_sum=0; int max1=0,pos1; for (int i=head[u];i;i=a[i].pre){ int v=a[i].to; ret+=last_sum*val[v]%mo; ret%=mo; last_sum+=val[v]%mo; last_sum%=mo; if (max1 v; } int max2=0; for (int i=head[u];i;i=a[i].pre) { int v=a[i].to; if (max2 val[v]; } ans1=max(ans1,max1*max2); ans2+=(ret<<1)%mo; ans2%=mo; } cout< ' '< '\n'; return 0; }
P1514 引水入城
给出$n \times m$的网格图每个点(i,j)表示该点的高度,然后考虑水只能从高处流向低处。
在第一行设k个取水站然后要求最后一行所有城市都可以流经水,若可能最小化k,若不可能求有多少个城市无法流经水
对于100%的数据$n,m \leq 500$
考虑一个$O(n^3)$算法求出第一行每一个点可以覆盖到最后一行多少点,我们可以证明覆盖的一定是一个区间:
若覆盖不是一个区间那么可能存在2条路径相交,而水可以选取任意一条路径流下,那么这两个区间不是独立的,
必然有交集而形成一个独立的1个区间。
然后在给出的若干个区间做线段覆盖若不能覆盖[1,n]计算30%的答案,若能覆盖那么计算剩余70%的答案
贪心做法如下:左端点排序,然后取头线段看后面头在头线段区间之间那么尽可能往右拓展。排序即可。
常数优化,当且仅当第一行高度比两边都高才进行搜索,显然更优(因为ta隔壁到的ta一定可以到)。
// luogu-judger-enable-o2 # includeusing namespace std; const int N=505; const int dx[]={-1,0,1,0}; const int dy[]={0,1,0,-1}; int mp[N][N],a[N][N],get[N]; int n,m,num; bool use[N][N]; struct node{int x,y;}; struct rec{int l,r;}w[N]; queue q; bool cmp(rec a, rec b){return a.l<b.l;} inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } void bfs(int sx,int sy) { memset(use,false,sizeof(use)); q.push((node){sx,sy}); use[sx][sy]=true; while (!q.empty()) { node u=q.front(); q.pop(); for (int i=0;i<4;i++) { node v; v.x=u.x+dx[i];v.y=u.y+dy[i]; if (v.x<1||v.x>n||v.y<1||v.y>m||use[v.x][v.y]) continue; if (a[u.x][u.y]<=a[v.x][v.y]) continue; use[v.x][v.y]=true; q.push(v); } } for (int i=1;i<=m;i++) if (use[n][i]) get[i]=true; int L=0,R=0; for (int i=1;i<=m;i++) if (use[n][i]) { L=i; break;} for (int i=m;i>=1;i--) if (use[n][i]) { R=i; break;} if (L==0&&R==0) return; w[++num]=(rec){L,R}; } int main() { n=read();m=read(); for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) a[i][j]=read(); for (int i=1;i<=m;i++) bfs(1,i); int ret=0; for (int i=1;i<=m;i++) if (get[i]) ret++; if (ret<m) { printf("0\n%d\n",m-ret); return 0; } sort(w+1,w+1+num,cmp); int i=1,j=1,ans=0; while (j<=m) { int t=0; while (w[i].l<=j) { t=max(t,w[i].r); i++; } j=t+1;ans++; } printf("1\n%d\n",ans); return 0; }
P1850 换教室
有n门课程,分在2n个教室上课c[i]和d[i],然后对教室之间有无向图{v,e}联通,每次从一教室走到另一教室走最短路。
第i门课程默认在c[i]上课,但是可以最多选择m门课程换到d[i]教室上课,有k[i]的期望换教室成功,确定申报若干个教室编号
使得完成n门课程期望行走路程最短,输出期望最短总路程长度。
对于100%的数据,$v \leq 300,n,m \leq 2000,$图可能会有重边和自环
考虑若选择当前教室申报和不选择当前教室申报期望是不同的,或者说一个有期望,一个没有期望。
所以要设计一维状态0/1表示当前教室选不选择申报。
所以显然的设计$f[i][j][0/1]$表示前 i 节课使用 j 次申报其中第 i 节课 不申报/申报 换教室最小期望总路程长度
考虑$f[i][j][0]$的转移,显然是从$i-1$转移而来,
第1种 第$i-1$节课不申请,所以对于$i-1$到$i$是唯一的一种可能期望路径 从$c[i-1]$走到$c[i]$,得$f[i-1][j][0]+dist(c[i-1],c[i])$
第2种 第$i-1$节课申请 , 所以对于$i-1$到$i$有2种可能的期望路径,从$c[i-1]$到$c[i]$($i-1$换课不成功),从$d[i-1]$到$c[i]$($i-1$换课成功),
得$f[i-1][j][1]+k[i-1]\times dist(d[i-1],c[i]) + (1-k[i-1]) \times dist(c[i-1],c[i])$
综上所述$f[i][j][0]=max\{ f[i-1][j][1]+k[i-1]\times dist(d[i-1],c[i]) + (1-k[i-1]) \times dist(c[i-1],c[i]) , f[i-1][j][0]+dist(c[i-1],c[i]) \}$
考虑$f[i][j][1]$的转移,显然是从$i-1$转移而来,
第1种 第$i-1$节课不申请,所以对于$i-1$到$i$有2种可能的期望路径,从$c[i-1]$到$c[i]$($i$换课不成功),从$c[i-1]$到$d[i]$($i$换课成功),
得$f[i-1][j-1][0]+k[i]\times dist(c[i-1],d[i])+(1-k[i])\times dist(c[i-1],d[i]) $
第2种 第$i-1$节课申请 , 所以对于$i-1$到$i$有$2 \times 2$种可能的期望路径,
从$c[i-1]$到$c[i]$($i-1,i$换课不成功),得 $(1-k[i-1]) \times (1-k[i]) \times dist(c[i-1],c[i])$
从$c[i-1]$到$d[i]$($i-1$换课不成功,$i$换课成功),得$((1-k[i-1])\times k[i] \times dist(c[i-1],d[i]) $
从$d[i-1]$到$c[i]$($i-1$换课成功,$i$换课不成功),得$k[i-1]\times (1-k[i]) \times dist(d[i-1],c[i])$
从$d[i-1]$到$d[i]$($i-1,i$换课成功),得$k[i-1] \times k[i] \times dist(d[i-1],d[i]) $
综上所述 $f[i][j][1]=max \{f[i-1][j-1][1]+(1-k[i-1]) \times (1-k[i]) \times dist(c[i-1],c[i]) +$
$ ((1-k[i-1])\times k[i] \times dist(c[i-1],d[i]) + $
$k[i-1]\times (1-k[i]) \times dist(d[i-1],c[i]) + $
$ k[i-1] \times k[i] \times dist(d[i-1],d[i]) $
$, f[i-1][j-1][0]+k[i]\times dist(c[i-1],d[i])+(1-k[i])\times dist(c[i-1],d[i]) \}$
然后考虑j=0的边界转移,若 k=0 只能从i-1不换课,转移,而k=1根本不能
边界f[1][0][0]=0,f[1][1][1]=0,f[][][]=INF
答案是$Min\{ f[n][i][0],f[n][i][1] \} (0 \leq i \leq m)$
# include# define fp(i,s,t) for (int i=s;i<=t;i++) # define INF (1<<20) using namespace std; const int N=2e3+10; int c[N],d[N]; double mp[N][N]; int n,m,v,e; double f[N][N][2],k[N]; double Min(double a,double b) { if (areturn a; else return b; } void floyd() { fp(k,1,v) fp(i,1,v) fp(j,1,v) if (k!=i&&i!=j&&j!=k) mp[i][j]=min(mp[i][j],mp[i][k]+mp[k][j]); } double D(int x,int y){return mp[x][y];} signed main() { scanf("%d%d%d%d",&n,&m,&v,&e); fp(i,1,n) scanf("%d",&c[i]); fp(i,1,n) scanf("%d",&d[i]); fp(i,1,n) scanf("%lf",&k[i]); fp(i,0,v) fp(j,0,v) mp[i][j]=(i==j||i==0||j==0?0:INF); fp(i,1,e) { int u,v; double w; scanf("%d%d%lf",&u,&v,&w); mp[u][v]=mp[v][u]=Min(mp[u][v],w); } floyd(); fp(i,0,n) fp(j,0,m) f[i][j][0]=f[i][j][1]=INF; f[1][0][0]=0.0; f[1][1][1]=0.0; fp(i,2,n) { f[i][0][0]=f[i-1][0][0]+D(c[i-1],c[i]); fp(j,1,min(i,m)) { f[i][j][0]=Min(f[i-1][j][0]+D(c[i-1],c[i]),f[i-1][j][1]+k[i-1]*D(d[i-1],c[i])+(1-k[i-1])*D(c[i-1],c[i]));//i没尝试去换 f[i][j][1]=Min(f[i-1][j-1][0]+k[i]*D(c[i-1],d[i])+(1-k[i])*D(c[i-1],c[i]), f[i-1][j-1][1]+k[i-1]*k[i]*D(d[i-1],d[i])+k[i-1]*(1-k[i])*D(d[i-1],c[i])+(1-k[i-1])*k[i]*D(c[i-1],d[i])+(1-k[i-1])*(1-k[i])*D(c[i-1],c[i])); } } double ans=INF; fp(i,0,m) ans=Min(ans,Min(f[n][i][0],f[n][i][1])); printf("%.2lf\n",ans); return 0; }
P1941 飞扬的小鸟
有n列m行的网格左下角是(0,0),小鸟每一单位时间横坐标右移1单位,纵坐标在时刻i(出发时为0时刻)有两种情况
- 1. 自由下落,纵坐标下降Y[i];
- 2. 花费k$(k >0)$的代价上升$k \times$X[i]。
注意一点:当点k次后小鸟高度超过m那么会停留在上边缘。
有k个管道,给出每个管道位置$p_i$,下表面距离x轴高度$L_i$,上表面距离x轴高度$H_i$
管道覆盖处(下表面高度以下,上表面高度以上)、地面(即x轴)不能触碰。
此时会有两种可能,
- 若从0列任意位置出发不可能到达n列,输出0\n,然后输出最多通过管道数
- 若从0列任意位置出发能够到达n列,输出1\n,然后输出最小代价(点击次数)
对于70%的数据,$ 5 \leq n\leq 10^3 ,5 \leq m\leq 10^2$
对于85%的数据,$X[i],Y[i]$较为随机,且开启O2常数优化
对于100%的数据,$5 \leq n \leq 10^4$, $5 \leq m \leq 10^3$,$0 \leq k < n$, $0 < X < m$, $0 < Y < m$, $0 < P < n$, $0 \leq L < H \leq m$, $L + 1 < H$
01背包和完全背包综合体。
我们会有一个简单的想法,暴力转移,对于以左下角作为(0,0)点的坐标系来说从i-1列到i列可以有两种转移方式。
记f[i][j]表示小鸟到(i,j)位置(即i列j高度)最小代价。
对于一般情况($j\neq m$),显然是从$f[i-1][j+Y[i-1]]$和$f[i-1][j-k\times X[i-1] ]+k$转移而来。
对于特殊情况($j=m$),只能从i-1列m及以下行某一处跳至少1次到达,跳1次跳到顶部$f[i-1][j-k]+1(k \in [j-X[i-1],m])$,跳2次以上跳到顶部$f[i][j-k]+1(k \in [j-X[i-1],m])$,即这次跳到(i,j-X[i-1])最小代价了如果再跳1次一定可以到达顶部。
复杂度O(knm),显然k取决于数据的随机程度,若给出X[i-1]较小那么程序复杂度会较大。
对于本题即使数据已经比较随机,给出X[]和Y[]以上算法在开启O2优化的情况下能够得70-85分
开启O2优化85分代码:
// luogu-judger-enable-o2 # include# define inf (0x3f3f3f3f) using namespace std; const int N=1e4+500,M=1e3+500; int f[N][M],down[N],up[N],x[N],y[N],pou[N]; int n,m,p; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } int main() { n=read();m=read();p=read(); for (int i=0;i ) x[i]=read(),y[i]=read(); for (int i=0;i<=n;i++) down[i]=0,up[i]=m+1; for (int i=1;i<=p;i++) { int pos=read(); down[pos]=read(); up[pos]=read(); pou[pos]=1; } memset(f,0x3f,sizeof(f)); for (int i=0;i<=m;i++) f[0][i]=0; int rp; for (int i=1;i<=n;i++) { for (int j=1;j<=m;j++) { if (j==m) { for (int k=m-x[i-1];k<=m;k++) { f[i][m]=min(f[i][m],f[i-1][k]+1); f[i][m]=min(f[i][m],f[i][k]+1); } } int k=1; while (true) { if (j-k*x[i-1]<=0||f[i][j]<=k) break; f[i][j]=min(f[i][j],f[i-1][j-k*x[i-1]]+k); k++; } } for (int j=1;j<=m;j++) if (j+y[i-1]<=m) f[i][j]=min(f[i][j],f[i-1][j+y[i-1]]); for (int j=1;j<=down[i];j++) f[i][j]=inf; for (int j=up[i];j<=m;j++) f[i][j]=inf; for (int j=1;j<=m;j++) if (f[i][j]!=inf) rp=i; } int Ans=inf; for (int i=1;i<=m;i++) Ans=min(Ans,f[n][i]); if (Ans!=inf) { printf("1\n%d\n",Ans); return 0; } Ans=0; for (int i=1;i<=rp;i++) if (pou[i]==1) Ans++; printf("0\n%d\n",Ans); return 0; }
考虑优化常数k,主要是做背包的时候枚举了个数k,可以考虑使用上述特判处的思想,从i这一列已经转移成功的那一行转移过来代替枚举的k,充分利用求得的东西,即f[i][j]从f[i][j-X[i-1]]+1转移过来,即跳到(i,j-X[i-1])已经最小化步数,再多点1次就跳到(i,j)了。
复杂度O(nm)
注意上述dp需要注意边界问题,不要出现负数下标,即使排除不可能决策的转移方案。
然后最后赋值水管位置为inf,两个背包分开来转移。
不开启O2优化100分代码:
# include# define inf (0x3f3f3f3f) using namespace std; const int N=1e4+50,M=1e3+50; int f[N][M],down[N],up[N],x[N],y[N],pou[N]; int n,m,p; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } int main() { n=read();m=read();p=read(); for (int i=0;i ) x[i]=read(),y[i]=read(); for (int i=0;i<=n;i++) down[i]=0,up[i]=m+1; for (int i=1;i<=p;i++) { int pos=read(); down[pos]=read(); up[pos]=read(); pou[pos]=1; } memset(f,0x3f,sizeof(f)); for (int i=0;i<=m;i++) f[0][i]=0; int rp; for (int i=1;i<=n;i++) { for (int j=1;j<=m;j++) { if (j==m) { for (int k=m-x[i-1];k<=m;k++) { f[i][m]=min(f[i][m],f[i-1][k]+1); f[i][m]=min(f[i][m],f[i][k]+1); } } if (j-x[i-1]>0) f[i][j]=min(f[i][j],f[i-1][j-x[i-1]]+1); if (j-x[i-1]>0) f[i][j]=min(f[i][j],f[i][j-x[i-1]]+1); } for (int j=1;j<=m;j++) if (j+y[i-1]<=m) f[i][j]=min(f[i][j],f[i-1][j+y[i-1]]); for (int j=1;j<=down[i];j++) f[i][j]=inf; for (int j=up[i];j<=m;j++) f[i][j]=inf; for (int j=1;j<=m;j++) if (f[i][j]!=inf) rp=i; } int Ans=inf; for (int i=1;i<=m;i++) Ans=min(Ans,f[n][i]); if (Ans<inf) { printf("1\n%d\n",Ans); return 0; } Ans=0; for (int i=1;i<=rp;i++) if (pou[i]==1) Ans++; printf("0\n%d\n",Ans); return 0; }