DP总结
短暂的一年OI学习将要告别,回想一年DP从无到有,想总结一番.
DP:最优子结构+阶段状态转移.
①:区间DP
其实感觉挺套路的,枚举区间长,L,R,即可.
T1:石子合并
sol:从这个题学习到了两点,一个就是拆环为链的技巧,二个就是我们发现要先枚举len,在枚举L,R,len从小到大,这就体现了DP无后效性阶段性的转移,然后对阶段取max即可.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register #define LL long long const LL inf = 1e11; LL n,Max,Min,val[301],sum[301],f[301][301],g[301][301]; inline LL fd(){ LL s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } int main() { n = fd(); for(re LL i=1;i<=n;++i){ val[i] = fd(); val[i+n] = val[i]; } n = (n<<1); memset(g,0,sizeof(g)); memset(f,0x7f,sizeof(f)); for(re LL i=1;i<=n;++i) sum[i] = sum[i-1]+val[i]; for(re LL i=1;i<=n;++i) f[i][i] = 0; for(re LL len=2;len<=n;++len){ for(re LL L=1;L+len-1<=n;++L){ LL R = L+len-1; for(re LL k=L;k<R;++k){ f[L][R] = min(f[L][R],f[L][k]+f[k+1][R]); g[L][R] = max(g[L][R],g[L][k]+g[k+1][R]); } f[L][R] += sum[R]-sum[L-1]; g[L][R] += sum[R]-sum[L-1]; } } Min = 1e11,Max = -1; n = (n>>1); for(re LL i=1;i<=n;++i){ Min = min(Min,f[i][i+n-1]); Max = max(Max,g[i][i+n-1]); } printf("%lld\n%lld",Min,Max); return 0; }
T2:Polygon
sol:照样拆环为链,按照区间DP,不过要多记录一维,f[L][R][0/1]表示区间最小/最大值,因为最大值可能由两个最小值(负数)相乘得来,最小值可能是最大值乘最小值(正 x 负).
#include<iostream> #include<cstring> #include<cstdio> #include<cmath> using namespace std; #define e exit(0) #define re register #define LL long long #define inf 1e11 #define Max(x,y)x>y?x:y #define Min(x,y)x<y?x:y const LL maxn = 51; char fh[maxn],bri[maxn][maxn]; LL n,l1,l2,num[maxn],f[maxn][maxn][2]; inline LL fd(){ LL s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } int main() { freopen("POJ1179.in","r",stdin); freopen("POJ1179.out","w",stdout); n = fd(); for(re LL i=1;i<=(n<<1);++i){ if(i&1) scanf("%c",&fh[++l1]); else{ num[++l2] = fd(); num[l2+n] = num[l2]; } } int id = 2; for(re LL i=1;i<=n;++i){ bri[i][i+1] = fh[id++]; if(id>n) id = 1; bri[i+n][i+1+n] = bri[i][i+1]; } n = (n<<1); for(re LL i=1;i<=n;++i) for(re LL j=1;j<=n;++j) f[i][j][0] = inf,f[i][j][1] = -inf; for(re LL i=1;i<=n;++i) f[i][i][0] = f[i][i][1] = num[i]; for(re LL len=2;len<=n;++len) for(re LL L=1;L+len-1<=n;++L){ int R = L+len-1; for(re LL k=L;k<R;++k){ if(bri[k][k+1] == 't'){ f[L][R][0] = min(f[L][R][0],f[L][k][0]+f[k+1][R][0]); f[L][R][1] = max(f[L][R][1],f[L][k][1]+f[k+1][R][1]); } else if(bri[k][k+1] == 'x'){ f[L][R][0] = min(f[L][R][0],f[L][k][1]*f[k+1][R][0]); f[L][R][0] = min(f[L][R][0],f[L][k][0]*f[k+1][R][1]); f[L][R][0] = min(f[L][R][0],f[L][k][0]*f[k+1][R][0]); f[L][R][0] = min(f[L][R][0],f[L][k][1]*f[k+1][R][1]); f[L][R][1] = max(f[L][R][1],f[L][k][1]*f[k+1][R][1]); f[L][R][1] = max(f[L][R][1],f[L][k][0]*f[k+1][R][0]); } } } n = (n>>1); LL ans = 0; for(re LL i=1;i<=n;++i) ans = max(ans,f[i][i+n-1][1]); printf("%lld\n",ans); for(re LL i=1;i<=n;++i) if(f[i][i+n-1][1] == ans) printf("%lld ",i); return 0; }
T3:能量项链
sol:照样拆环为链,但这个题比较诡异的地方就是注意拆环为链后头尾坐标,转移的时候注意head[L]*tail[k]*tail[R].
#include<iostream> #include<cstdio> using namespace std; #define R register int n,ans=-1,head[310],tail[310],f[310][310]; int main() { // freopen("s.in","r",stdin); // freopen("s.out","w",stdout); scanf("%d",&n); for(R int i=1;i<=n;++i) { scanf("%d",&head[i]); head[i+n]=head[i]; } for(R int i=1;i<=2*n-1;++i) tail[i]=head[i+1]; tail[2*n]=head[1]; for(R int i=1;i<=2*n-1;++i) f[i][i]=0; for(R int len=1;len<=n-1;++len){ for(R int i=1;i<=2*n-len;++i){ int j=i+len; for(R int k=i;k<=j-1;++k) f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]*tail[k]*tail[j]); } } for(R int i=1;i<=n;++i) ans=max(ans,f[i][i+n-1]); printf("%d",ans); return 0; }
T4:凸多边形的三角划分
给定一具有 N 个顶点从 1 到 N 编号的凸多边形,每个顶点的权均已知.问如何把 这个凸多边形划分成 N-2 个互不相交的三角形,使得这些三角形顶点的权的乘积之和最小?
忽略这张大图
sol:还是区间DP套路,f[L][R]表示顺序划分[L,R]子多边形最小的权值,转移时注意f[L][R] = min{f[L][k]+f[k][R]+val[L]*val[R]*val[k]}.显然这个题的难点是我们要将点'强行'定编号确定后效形,还要注意细节:k∈[L+1,R-1].
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register #define LL long long long long n,val[110],f[110][110]; long long getv(LL x,LL y,LL z){return val[x]*val[y]*val[z];} int main() { freopen("division.in","r",stdin); freopen("division.out","w",stdout); scanf("%lld",&n); for(re LL i=1;i<=n;++i) scanf("%lld",&val[i]); memset(f,0x7f,sizeof(f)); for(re LL len=2;len<=n;++len) for(re LL i=1;i+len-1<=n;++i){ LL j=i+len-1; if(j==i+1) f[i][j]=0; for(re LL k=i+1;k<=j-1;++k) f[i][j]=min(f[i][j],f[i][k]+f[k][j]+getv(i,k,j)); } printf("%lld",f[1][n]); return 0; }
T5:看这里的T4,T3.
②状压DP:
做过的题都觉的挺套路的,难点都在如何用二进制运算去满足条件.
T1:广场铺砖
sol:其实这个题算状压DP'状态设计'中比较有意思的题.设横放的为'0',竖放的的为'1',这样做是为了上下转移判断合法,s3 = s1|s2,s3只能有偶数个'1'.且s1,s2上下不能同为1.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define R register #define ll long long int main() long long W,H,num,sum,f[13][2050],check[2050]; { freopen("floor.in","r",stdin); freopen("floor.out","w",stdout); scanf("%lld%lld",&W,&H); if((W&1)&&(H&1)){ printf("0"); return 0; } for(R int i=0;i<(1<<H);++i){ num=sum=0; for(R int j=0;j<H;++j) if((1<<j)&i) num|=sum,sum=0; else sum^=1; check[i]=num|sum ? 0: 1; } f[0][0]=1; for(R ll i=1;i<=W;++i) for(R ll j=0;j<(1<<H);++j) { f[i][j]=0; for(R ll k=0;k<(1<<H);++k) if(!(j&k)&&check[j|k]) f[i][j]+=f[i-1][k]; } printf("%lld",f[W][0]); return 0; }
T2:玉米地
sol:f[x][sta]第x行且状态为sta的总方案.涉及到状压DP计数的一个模板.只要上下&并不冲突,并不包含'贫瘠土地'即可.
#include<iostream> #include<cstring> #include<cstdio> #include<vector> using namespace std; #define e exit(0) #define re register #define LL long long const LL maxn = (1<<13); const LL mod = 100000000; vector<LL> q[20]; LL m,n,Maxm,Maxn,ans,ma[20][20],f[20][maxn]; inline LL fd(){ LL s=1,t=0; char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } bool checkone(LL x,LL statue){ bool flag = (statue & statue<<1)>0; if(flag) return false; LL len = q[x].size(); for(re LL i=0;i<len;++i){ LL y = q[x][i]; if((1<<y)&statue) return false; } return true; } bool checktwo(LL s1,LL s2){ if(s1&s2) return false; return true; } int main() { m = fd(),n = fd(); Maxm = (1<<m),Maxn = (1<<n); for(re LL i=0;i<m;++i) for(re LL j=0;j<n;++j){ ma[i][j] = fd(); if(ma[i][j] == 0) q[i].push_back(j); } f[0][0] = 1; for(re LL statue=1;statue<Maxn;++statue){ if(!checkone(0,statue)) continue; f[0][statue] = 1; } for(re LL i=1;i<m;++i){ for(re LL s1=0;s1<Maxn;++s1){ if(!checkone(i,s1)) continue; for(re LL s2=0;s2<Maxn;++s2){ if(!checktwo(s1,s2)) continue; f[i][s1] = (f[i][s1]+f[i-1][s2])%mod; } } } for(re LL statue=0;statue<Maxn;++statue) ans = (ans+f[m-1][statue])%mod; printf("%lld",ans); return 0; }
T3:互不侵犯
sol:f[x][sta][k]第x行且状态为sta,总共放了k个国王的方案.其实就'玉米地'多去判断&后的相邻位置是否都为'1'.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register #define LL long long const LL maxn = (1<<10); LL n,k,Maxn,ans,num[maxn],f[10][100][maxn]; inline LL fd(){ LL s=1,t=0; char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } LL work(LL x){ LL cnt = 0; while(x){ ++cnt; x = x&(x-1); } return cnt; } bool checkone(LL x){return (x&x<<1)>0;} bool checktwo(LL s1,LL s2){ if(s1&s2) return false; LL s3 = s1|s2; if(checkone(s3)) return false; return true; } int main() { n = fd(),k = fd(); Maxn = (1<<n); for(re LL statue = 0;statue<Maxn;++statue) num[statue] = work(statue); f[0][0][0] = 1; for(re LL statue = 1;statue<Maxn;++statue){ if(checkone(statue)) continue; f[0][num[statue]][statue] = 1; } for(re LL i=1;i<n;++i) for(re LL s1=0;s1<Maxn;++s1){ if(checkone(s1)) continue; for(re LL s2=0;s2<Maxn;++s2){ if(checkone(s2)) continue; if(!checktwo(s1,s2)) continue; LL s = num[s1]; for(re LL sum=0;sum<=i*n;++sum){ if(!f[i-1][sum][s2]) continue; LL nows = sum+s; f[i][nows][s1] += f[i-1][sum][s2]; } } } for(re LL s = 0;s<Maxn;++s) ans += f[n-1][k][s]; printf("%lld",ans); return 0; }
T4:炮兵阵地
sol:可以设计f[x][s1][s2],表示第x行,此行状态为s1,x-1行状态为s2,可以合法放的最多炮兵个数.其实可以提前处理好对于第x行合法的x-1行,x-2行,循环的次数会减少,'s2'也可以认为第几个合法的状态,这样时间空间都省下来了,对于每一行,也要判断左右相邻两格都不为1.
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> #include<vector> using namespace std; #define e exit(0) #define re register #define LL int const LL maxn = (1<<10); vector<LL> D_one[101]; vector<LL> D_two[maxn]; char c[101][10]; LL n,m,Maxm,ans,vis[maxn],num[maxn],f[100][550][550]; inline LL fd(){ LL s=1,t=0; char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } bool checkone(LL x){ LL q[24],cnt = 0,id = 0; bool flag = (x&x<<1)>0; if(flag) return false; while(x){ ++id; LL num = x%2; if(num == 1) q[++cnt] = id; x/=2; } sort(q+1,q+1+cnt); for(re LL i=2;i<=cnt;++i) if(q[i] - q[i-1]<=2) return false; return true; } bool checktwo(LL x,LL s){ if(!checkone(s)) return false; LL len = D_one[x].size(); for(re LL i=0;i<len;++i){ LL t = D_one[x][i]; if((1<<t)&s) return false; } return true; } bool checkIII(LL s1,LL s2){ if(s1&s2) return false; return true; } LL work(LL x){ LL cnt = 0; while(x){ ++cnt; x = x&(x-1); } return cnt; } int main() { n = fd(),m = fd(); for(re LL i=0;i<n;++i) for(re LL j=0;j<m;++j){ cin>>c[i][j]; if(c[i][j] == 'H') D_one[i].push_back(m-j-1); } Maxm = (1<<m); for(re LL s = 0;s<Maxm;++s) num[s] = work(s); for(re LL i=0;i<n;++i) for(re LL s=0;s<Maxm;++s){ if(!checktwo(i,s)) continue; D_two[i].push_back(s); } LL len = D_two[0].size(); if(n == 1){ for(re LL i=0;i<len;++i){ LL s = D_two[0].size(); ans = max(ans,num[s]); } printf("%d",ans); return 0; } for(re LL i=0;i<D_two[1].size();++i){ LL s1 = D_two[1][i]; for(re LL j=0;j<D_two[0].size();++j){ LL s2 = D_two[0][j]; if(!checkIII(s1,s2)) continue; f[1][i][j] = num[s1]+num[s2]; } } for(re LL i=2;i<n;++i){ LL len1 = D_two[i].size(); for(re LL a=0;a<len1;++a){ LL s1 = D_two[i][a]; LL len2 = D_two[i-1].size(); for(re LL b=0;b<len2;++b){ LL s2 = D_two[i-1][b]; if(!checkIII(s1,s2)) continue; LL len3 = D_two[i-2].size(); for(re LL c=0;c<len3;++c){ LL s3 = D_two[i-2][c]; if(!checkIII(s2,s3)||!checkIII(s1,s3)) continue; f[i][a][b] = max(f[i][a][b],f[i-1][b][c]+num[s1]); } } } } LL len1 = D_two[n-1].size(),len2 = D_two[n-2].size(); for(re LL i=0;i<len1;++i) for(re LL j=0;j<len2;++j) ans = max(ans,f[n-1][i][j]); printf("%d",ans); return 0; }
T5:愤怒的小鸟
sol:f[x]表示状态为x的用的最少的小鸟.我们任选两个合法的小鸟作为一条抛物线,找出状态s'表示这个抛物线能打到的猪.然后对每一个状态暴力用每一条抛数线更新即可.
#include<iostream> #include<cstring> #include<cstdio> #include<cmath> using namespace std; #define e exit(0) #define re register #define LD double const int maxn = (1<<19); int T,n,m,Maxn,f[maxn],a[20][20]; struct DL{LD x,y;}Q[20]; LD Fabs(LD a){return a>0?a:-a;} int main() { scanf("%d",&T); while(T--){ memset(a,0,sizeof(a)); memset(f,0x3f,sizeof(f)); scanf("%d%d",&n,&m); Maxn = (1<<n); for(re int i=0;i<n;++i) scanf("%lf%lf",&Q[i].x,&Q[i].y); for(re int i=0;i<n;++i) a[i][i] |= (1<<i); for(re int i=0;i<n;++i) for(re int j=i+1;j<n;++j){ if(fabs(Q[i].x-Q[j].x)>1e-6){ LD x1 = Q[i].x,y1 = Q[i].y,x2 = Q[j].x,y2 = Q[j].y; LD A = (x2*y1-x1*y2)/((x1-x2)*x1*x2); LD B = (y1-A*x1*x1)/x1; if(A > -1e-6) continue; for(re int k=0;k<n;++k){ LD x = Q[k].x,y = Q[k].y; if(fabs(A*x*x+B*x-y)<=1e-6) a[i][j] |= (1<<k); } } } f[0] = 0; for(re int i=0;i<Maxn;++i){ for(re int j=0;j<n;++j) for(re int k=j;k<n;++k) f[i|a[j][k]] = min(f[i|a[j][k]],f[i]+1); } printf("%d\n",f[Maxn-1]); } return 0; }
T6:骑士
国际象棋中骑士的移动规则和中国象棋中的马是类似的,它先沿着一个方向移动两格,再沿着与刚才移动方向垂直的方向移动一格.路径上的棋子并不会影响骑士的移动,但是如
果一个骑士走到了一个放有棋子的格子,它就会攻击那个棋子.现在有一个 n*n 的棋盘,有 k 个骑士需要被摆到棋盘上去.那么使所有骑士互不攻击的摆放方式一共有多少种呢?
首先你要弄清骑士的攻击范围
然后这个题就完了.
#include<iostream> #include<cstring> #include<cstdio> #include<vector> using namespace std; #define e exit(0) #define re register const int maxn = (1<<8); int n,k,Maxn; double f[8][maxn][maxn][65]; vector<int> Q[maxn];vector<int> G[maxn]; inline int fd(){ int s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } bool checkone(int s1,int s2){ int s4 = (s1<<2),s5 = (s1>>2); if((s2&s4)||(s2&s5)) return false; return true; } bool checktwo(int s1,int s2){ int s4 = (s1<<1),s5 = (s1>>1); if((s2&s4)||(s2&s5)) return false; return true; } int getsum(int x){ int cnt = 0; while(x){++cnt;x = x&(x-1);} return cnt; } void propre(){ memset(f,0,sizeof(f)); for(re int i=0;i<Maxn;++i){ int t = getsum(i); f[0][i][0][t] = 1; } for(re int s1=0;s1<Maxn;++s1) for(re int s2=0;s2<Maxn;++s2){ if(!checkone(s1,s2)) continue; int t1 = getsum(s1),t2 = getsum(s2); f[1][s1][s2][t1+t2] += f[0][s2][0][t2]; } } void slove(){ for(re int i=2;i<n;++i){ for(re int sta=0;sta<Maxn;++sta){ int len1 = Q[sta].size(),len2 = G[sta].size(),p = getsum(sta); for(re int x=0;x<len1;++x){ int s1 = Q[sta][x]; for(re int y=0;y<len2;++y){ int s2 = G[sta][y]; if(!checkone(s1,s2)) continue; for(re int t=0;t<=k;++t){ int s = t+p; f[i][sta][s1][s] += f[i-1][s1][s2][t]; } } } } } double ans = 0; for(re int s1=0;s1<Maxn;++s1) for(re int s2=0;s2<Maxn;++s2){ ans += f[n-1][s1][s2][k]; } printf("%.0lf",ans); } int main() { freopen("knight.in","r",stdin); freopen("knight.out","w",stdout); n = fd(),k = fd(); Maxn = (1<<n); for(re int i=0;i<Maxn;++i) for(re int j=0;j<Maxn;++j){ if(!checkone(i,j))continue; Q[i].push_back(j); } for(re int i=0;i<Maxn;++i) for(re int j=0;j<Maxn;++j){ if(!checktwo(i,j)) continue; G[i].push_back(j); } propre();slove(); return 0; }
③树形DP:
可以想象树形父子关系的每一层就是一个DP转移阶段,天然后效性.
T1:没有上司的舞会
sol:f[x][0/1]表示第x个节点选不选,保证x即x的子树都合法的前提下,能得到的最大值快乐值.可知当x选时,x的儿子必不选,x不选时,x的儿子可选可不选.
#include<iostream> #include<cstdlib> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register const int maxn = 6010; int n,cnt,rt,U[maxn],head[maxn],val[maxn],f[maxn][2]; struct bian{int to,next;}len[maxn]; inline int fd(){ int s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } void add(int from,int to){ len[++cnt].to = to; len[cnt].next = head[from]; head[from] = cnt; } void dp(int x){ for(re int k=head[x];k;k=len[k].next){ int to = len[k].to; dp(to); f[x][1] += f[to][0]; f[x][0] += max(f[to][1],f[to][0]); } f[x][1] += val[x]; } int main() { n = fd(); for(re int i=1;i<=n;++i) val[i] = fd(); for(re int i=1;i<=n-1;++i){ int x = fd(),y = fd(); add(y,x); ++U[x]; } int x = fd(),y = fd(); for(re int i=1;i<=n;++i) if(!U[i]) rt = i; dp(rt); printf("%d",max(f[rt][0],f[rt][1])); return 0; }
T2:选课
sol:很经典的树上泛化背包问题,对此可以应用于多叉树,并不只限于二叉树,在code中有重要解释.
#include<iostream> #include<cstring> #include<cstdio> #include<cmath> using namespace std; #define e exit(0) #define re register const int maxn = 401; int n,m,cnt,val[maxn],head[maxn],f[maxn][maxn]; struct bian{int to,next;}len[maxn]; inline int fd(){ int s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } void add(int from,int to){ len[++cnt].to = to; len[cnt].next = head[from]; head[from] = cnt; } void dp(int x){ f[x][0] = 0; for(re int k=head[x];k;k=len[k].next){ int to = len[k].to; dp(to); //倒序,类似于01背包的先后更新顺序. for(re int t=m;t>=0;--t) for(re int j=0;j<=t;++j){//倒序顺序即可. if(t-j>=0) f[x][t] = max(f[x][t],f[x][t-j]+f[to][j]); } } if(x != 0) for(re int t=m;t>0;--t) f[x][t] = f[x][t-1]+val[x];//不能取max,f[x][t]未更新前是儿子选t的状态,并不是它自己选t的状态 } int main() { n = fd(),m = fd(); for(re int i=1;i<=n;++i){ int x = fd();val[i] = fd(); int y = i; add(x,y); } dp(0); printf("%d",f[0][m]); return 0; }
T3:通往自由的钥匙
通向自由的钥匙被放 n 个房间里,这 n 个房间由 n-1 条走廊连接.但是每个房间里都有 特别的保护魔法,在它的作用下,我无法通过这个房间,也无法取得其中的钥匙.虽然我可
以通过消耗能量来破坏房间里的魔法,但是我的能量是有限的.那么,如果我最先站在 1 号房间(1 号房间的保护魔法依然是有效的,也就是,如果不耗费能量,我无法通过 1 号房间,也无法取得房间中的钥匙),如果我拥有的能量为 P,我最多能取得多少钥匙?
sol:开始感觉与上面一题相似,其实不然,由于这个背包带权,很多细节不一样,在code中重要解释.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register const int N = 301; int n,p,cnt,cost[N],v[N],head[N],f[N][N]; struct bian{int to,next;}len[N]; inline int fd(){ int s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } void add(int from,int to){ len[++cnt].to = to; len[cnt].next = head[from]; head[from] = cnt; } void Dfs(int x,int fa){ for(re int j=p;j>=cost[x];--j) f[x][j] = max(f[x][j],f[x][j-cost[x]]+v[x]); //提前将v[x]的影响加上,先强制选x. for(re int k=head[x];k;k=len[k].next){ int to = len[k].to; if(to == fa) continue; Dfs(to,x); //为什么一定要大于等于cost[x],而不是像上一个题大于等于0?由于这个背包带权,我们不能让不合法的状态(<cost[x])有值. //并且有强制选x的意思. for(re int m=p;m>=cost[x];--m) for(re int t=m;t>=cost[x];--t) f[x][m] = max(f[x][m],f[x][t]+f[to][m-t]); } } int main() { freopen("key.in","r",stdin); freopen("key.out","w",stdout); n = fd(),p = fd(); for(re int i=1;i<=n;++i) cost[i] = fd(),v[i] = fd(); for(re int i=1;i<=n-1;++i){ int x = fd(),y = fd(); add(x,y),add(y,x); } Dfs(1,0); printf("%d",f[1][p]); return 0; }
T4:警卫安排
一个重要的基地被分为 n 个连通的区域.出于某种神秘的原因,这些区域以一个区域为核心,呈一颗树形分布.在每个区域安排警卫所需要的费用是不同的,而每个区域的警卫都可以望见其相邻的区域,只要一个区域被一个警卫望见或者是安排有警卫,这个区域就是安全的.你的任务是在确保所有区域都是安全的情况下,找到安排警卫的最小费用.
sol:T1的升级版.f[x][0/1/2]表示它被父亲,自己,儿子选,保证它即它的子树都合法的前提下,安排警卫的最小费用,f[x][0]可以转移f[son][1/2],f[x][1]可以转移f[son][0/1/2],f[x][2]则要强制选一个儿子,其他选最优情况.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register const int maxn=730; int n,m,from,to,root,cnt,val[maxn],head[maxn],du[maxn],cu[maxn],f[maxn][3]; struct bian{ int to,next; }len[maxn<<1]; inline int fd(){ int s=1,t=0; char c=getchar(); while(c<'0'||c>'9'){ if(c=='-') s=-1; c=getchar(); } while(c>='0'&&c<='9'){ t=t*10+c-'0'; c=getchar(); } return s*t; } void add(int from,int to){ len[++cnt].to=to; len[cnt].next=head[from]; head[from]=cnt; } void dp(int x){ f[x][1] = val[x]; f[x][2] = 0; for(re int k=head[x];k;k=len[k].next){ int to=len[k].to; dp(to); f[x][2] += min(f[to][1],f[to][0]); f[x][1] += min(f[to][0],min(f[to][1],f[to][2])); } int ans = 2139062143; for(re int k=head[x];k;k=len[k].next){ int to=len[k].to; int v=f[to][1]; for(re int g=head[x];g;g=len[g].next){ int t=len[g].to; if(t==to) continue; v += min(f[t][0],f[t][1]); } ans = min(ans,v); } f[x][0] = ans; } int main() { freopen("security.in","r",stdin); freopen("security.out","w",stdout); n=fd(); for(re int i=1;i<=n;++i){ from=fd(),val[from]=fd(),m=fd(); for(re int i=1;i<=m;++i){ to=fd(),add(from,to); ++du[to],++cu[from]; } } for(re int i=1;i<=n;++i) if(!du[i]){ root=i; break; } memset(f,0x7f,sizeof(f)); dp(root); printf("%d",min(f[root][0],f[root][1])); return 0; }
T5:消防局的设立
sol:T4的升级版,多分情况讨论即可.设f[x][0/1/2/3/4].f[x][0]保证x的爷爷,x的父亲,x,即x的子树都安全的最少消防局数量;f[x][1]保证x的父亲,x,即x的子树都安全的最少消防局数量;f[x][0]保证x即x的子树都安全的消防局数量...要注意这里的f[x][0/1/2/3/4]与T4的定义是不同的.接下来的转移就贪心地分类讨论即可,借鉴一下LG题解.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register #define inf 19260817 const int maxn = 1010; int n,cnt,head[maxn],f[maxn][5]; struct bian{int to,next;}len[maxn]; inline int fd(){ int s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } void add(int from,int to){ len[++cnt].to = to; len[cnt].next = head[from]; head[from] = cnt; } void DP(int x){ f[x][0] = 1,f[x][3] = f[x][4] = 0; for(re int k=head[x];k;k=len[k].next){ int to = len[k].to; DP(to); f[x][0] += f[to][4]; f[x][3] += f[to][2]; f[x][4] += f[to][3]; } if(head[x] == 0) f[x][0] = f[x][1] = f[x][2] = 1; else{ f[x][1] = f[x][2] = inf; for(re int k=head[x];k;k=len[k].next){ int s = len[k].to; int v1 = f[s][0],v2 = f[s][1]; for(re int g=head[x];g;g=len[g].next){ int t = len[g].to; if(s == t) continue; v1 += min(f[t][2],f[t][3]),v2 += f[t][2]; } f[x][1] = min(f[x][1],v1); f[x][2] = min(f[x][2],v2); } } for(re int i=1;i<=4;++i) f[x][i] = min(f[x][i],f[x][i-1]); } int main() { n = fd(); for(re int i=2;i<=n;++i){ int x = fd(),y = i; add(x,y); } DP(1); printf("%d",f[1][2]); return 0; }
T6:这里的T4.
④背包:
看似最基础的,但个人认为是最重要的.
T1:01背包.
sol:主要注意倒序转移,用之前的状态更新当前的状态,即代表物品在这个阶段只用一次.
#include<iostream> #include<cstdio> using namespace std; #define R register const int maxn=1010; int v,m,ti[maxn],val[maxn],f[maxn]; inline int fd() { int s=1,t=0; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') s=-1; c=getchar(); } while(c>='0'&&c<='9') { t=t*10+c-'0'; c=getchar(); } return s*t; } int main() { //freopen("s.in","r",stdin); //freopen("s.out","w",stdout); v=fd(),m=fd(); for(R int i=1;i<=m;++i) ti[i]=fd(),val[i]=fd(); for(R int i=1;i<=m;++i) for(R int j=v;j>=ti[i];--j) f[j]=max(f[j],f[j-ti[i]]+val[i]); printf("%d",f[v]); return 0; }
T2:完全背包.
sol:注意正序转移,用现在的状态更新现在状态,即代表物品在这个阶段使用多次.
#include<iostream> #include<cstdio> using namespace std; #define R register const int maxn=1e5+10; int v,m,f[maxn],ti[maxn],val[maxn]; inline int fd() { int s=1,t=0; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') s=-1; c=getchar(); } while(c>='0'&&c<='9') { t=t*10+c-'0'; c=getchar(); } return s*t; } int main() { // freopen("s.in","r",stdin); // freopen("s.out","w",stdout); v=fd(),m=fd(); for(R int i=1;i<=m;++i) ti[i]=fd(),val[i]=fd(); for(R int i=1;i<=m;++i) for(R int j=ti[i];j<=v;++j) f[j]=max(f[j],f[j-ti[i]]+val[i]); printf("%d",f[v]); return 0; }
T3:01背包变形
en,没有题面,就是将01背包的容积扩大到1e9范围,但Σ价值的范围控制在1e6内.
sol:改一下f[i]定义即可,f[i]表示能达到i价值时使用的背包最小值.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register #define inf 19260817 const int maxn = 3e5; int t,m,sumc,v[maxn],c[maxn],f[maxn]; inline int fd(){ int s=1,t=0; char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } int main() { t = fd(),m = fd(); for(re int i=1;i<=m;++i){ v[i] = fd(),c[i] = fd(); sumc += c[i]; } for(re int i=1;i<=sumc;++i) f[i] = inf; f[0] = 0; for(re int i=1;i<=m;++i) for(re int j=sumc;j>=c[i];--j){ f[j] = min(f[j],f[j-c[i]]+v[i]); } for(re int i=sumc;i>=0;--i){ if(f[i]<=t){ printf("%d",i); break; } } return 0; }
T4:完全背包变形
sol:选择最小物品,利用迪杰斯特拉,去构造关于它的剩余系即可(我可能在滥用'剩余系').
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> #include<queue> using namespace std; #define e exit(0) #define re register #define inf 19260817 const int maxn = 6e4; int n,mod,q,a[maxn+10],dis[maxn+10]; inline int fd(){ int s=1,t=0; char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } void dj(){ for(re int i=0;i<maxn;++i) dis[i] = inf; dis[0] = 0; priority_queue<pair<int,int> >q; q.push(make_pair(0,0)); while(q.size()){ int v = q.top().first; int now = q.top().second; q.pop(); v = -v; if(dis[now]!=v) continue; for(re int i=2;i<=n;++i){ int id = (now+a[i])%mod; if(dis[id] > dis[now]+a[i]){ dis[id] = dis[now]+a[i]; q.push(make_pair(-dis[id],id)); } } } } int main() { n = fd(); for(re int i=1;i<=n;++i) a[i] = fd(); sort(a+1,a+1+n); mod = a[1]; dj(); q = fd(); for(re int i=1;i<=q;++i){ int num = fd(); if(dis[num%mod]>num) printf("No\n"); else printf("Yes\n"); } return 0; }
T5:多重背包
给定N种物品,其中第i种物品的体积为vi,价值为wi,并且有ci个.有一个容积为M的背包,选择若干个物品选入,体积不超过M,求最大价值.
sol:可以暴力拆ci为1,在做01背包,也可以将ci 二进制拆分,利用唯一分解定理,也可做.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register const int M = 10001; int n,lx,cost[M],num[M],val[M]; struct DL{int v,c;}Q[M<<2]; inline int fd(){ int s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } int main() { freopen("kkk.in","r",stdin); freopen("kkk.out","w",stdout); n = fd(); for(re int i=1;i<=n;++i) cost[i] = fd(),val[i] = fd(),num[i] = fd(); for(re int i=1;i<=n;++i){ for(re int j=1;j<=num[i];j<<1){ Q[++lx].c = val[i]*j; Q[lx].v = j*cost[i]; num[i] -= j; } if(num[i] > 0){ Q[++lx].c = val[i]*num[i]; Q[lx].v = num[i]*j; } } return 0; }
T6:分组背包
sol:将组的类别看做转移的阶段,类比于01背包,但要注意颜色类别,体积,同一颜色物品三者顺序不能换.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register int V,n,c,f[1001],vis[101],len[101]; struct BP{int cost,v;}Q[101][1001]; inline int fd(){ int s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } int main() { freopen("kkk.in","r",stdin); freopen("kkk.out","w",stdout); V = fd(),n = fd(); for(re int i=1;i<=n;++i){ int cost = fd(),v = fd(),col = fd(); Q[col][++len[col]].cost = cost; Q[col][len[col]].v = v; if(!vis[col]){ vis[col] = 1; ++c; } } for(re int i=1;i<=c;++i){ for(re int m=V;m>=0;--m) for(re int k=1;k<=len[i];++k){ //if(m < cost) continue; int cost = Q[i][k].cost,v = Q[i][k].v; if(m < cost) continue; f[m] = max(f[m],f[m-cost]+v); } } printf("%d",f[V]); return 0; }
T7:coin
sol:如果强行用二进制拆分写会超时,单调队列? 没试过.设f[i]表示i面值是否能被表示,used[j]表示f[j]变为1在i阶段最少用多少枚硬币.同时考虑到这个题的贪心性质.前i种硬币能拼成面值j只有两种情况:①在i-1及以前的阶段就使f[j]变成1.②在i阶段,存在f[j-a[i]]变为1,used[j-a[i]]<c[i].
#include<iostream> #include<cstring> #include<cstdio> #include<cmath> using namespace std; #define e exit(0) #define re register const int maxn = 1e5+5; int n,m,a[maxn],b[maxn],f[maxn],used[maxn]; inline int fd(){ int s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } int main() { while(1){ n = fd(),m = fd(); if(n == 0&&m == 0) break; for(re int i=1;i<=n;++i) a[i] = fd(); for(re int i=1;i<=n;++i) b[i] = fd(); f[0] = 1; for(re int i=1;i<=n;++i){ for(re int j=0;j<=m;++j) used[j] = 0; for(re int j=a[i];j<=m;++j) if(!f[j]&&f[j-a[i]]&&used[j-a[i]] < b[i]) f[j] = 1,used[j] = used[j-a[i]]+1; } int ans = 0; for(re int i=1;i<=m;++i) if(f[i]){ ans += 1; f[i] = 0; } printf("%d\n",ans); } return 0; }
T8:这里T3
sol:经典完全背包找第k大问题.
⑤:期望DP
E(x) = 总和/总方案数. E(x) = 单次方案概率*单次方案和.
期望是满足线性递推关系的.
T1:绿豆蛙的归宿
sol:这个题首先要明白要逆推,应为终点到终点的期望距离肯定是0,也就是我们已知了终点的期望状态.那我们建反向图,做一遍拓扑排序,按照题意递推即可.
#include<iostream> #include<cstring> #include<cstdio> #include<queue> using namespace std; #define e exit(0) #define D double #define re register const int maxn = 1e5+10; double f[maxn],ans; int n,m,cnt,head[maxn],du[maxn],degree[maxn]; struct bian{int to,next,v;}len[maxn<<1]; inline int fd(){ int s=1,t=0; char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } void add(int from,int to,int v){ len[++cnt].v = v; len[cnt].to = to; len[cnt].next = head[from]; head[from] = cnt; } int main() { n = fd(),m = fd(); for(re int i=1;i<=m;++i){ int from = fd(),to = fd(),v = fd(); add(to,from,v); ++du[from],++degree[from]; } queue<int> q; q.push(n); while(q.size()){ int now = q.front(); q.pop(); for(re int k=head[now];k;k=len[k].next){ int to = len[k].to; f[to] += (f[now]+len[k].v)/degree[to]; --du[to]; if(du[to] == 0) q.push(to); } } printf("%.2lf",f[1]); return 0; }
T2:涂色.
ps:只会一个很暴力的办法.
sol:首先设计f[s]为涂色涂成状态为s的概率,那么分k个阶段,每次选择两个点作为染色区间去暴力覆盖s的都新的状态s',注意s必须倒序(与01背包倒序原因一致),每次都成上n^2的逆元即可,最后累加状态s中1的个数 x f[s]即可.
#include<iostream> #include<cstring> #include<cstdio> using namespace std; #define e exit(0) #define re register #define LL long long const LL mod = 998244353; const LL maxn = (1<<16); LL n,k,ans,Maxn,Ins,f[maxn]; inline LL fd(){ LL s=1,t=0;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')s=-1;c=getchar();} while(c>='0'&&c<='9'){t=t*10+c-'0';c=getchar();} return s*t; } LL qsm(LL x,LL y){ LL base = 1; while(y){ if(y&1) base = (base%mod*x%mod)%mod; x = (x%mod*x%mod)%mod; y>>=1; } return base; } LL getnum(LL x){ LL cnt = 0; while(x){ ++cnt; x = x&(x-1); } return cnt; } int main() { freopen("paint.in","r",stdin); freopen("paint.out","w",stdout); n = fd(),k = fd(); Maxn = (1<<n); f[0] = 1,Ins = qsm(n*n,mod-2); for(re LL i=1;i<=k;++i) for(re LL sta=Maxn;sta>=0;--sta){ LL ret = f[sta]; for(re LL a=0;a<n;++a) for(re LL b=0;b<n;++b){ LL x = a,y = b; if(x > y) swap(x,y); LL s1 = (1<<x)-1,s2 = (1<<y+1)-1,s3,s4; s3 = (s1^s2),s4 = (sta|s3); f[s4] = (f[s4]%mod+(ret*Ins%mod)%mod)%mod; } } for(re LL sta=0;sta<Maxn;++sta) ans = (ans%mod+(getnum(sta)%mod*f[sta]%mod)%mod)%mod; printf("%lld",ans); return 0; }