10.Paths through the Hourglass,UVa10564
题意:
有一个沙漏,第一行有\(n\)个格子,第二行有\(n-1\)个格子\(\cdots\cdots\)最中间的行只有1个格子,然后它下面一行2个格子,再下面一行3个格子\(\cdots\cdots\)最后一行\(n\)个格子,如图\(1-60\)所示。你可以从第一行开始往下走,每次往下走一行,往左或往右走一列,但不能走出沙漏。你的目标是让沿途经过的所有整数之和恰好为一个给定的整数\(S\)。求出符合上述条件的路径条数和一条路径。
如果有多条路径,起点编号应尽量小(第一行格子从左到右编号为0~n-1)。如果仍有多解,移动序列(\(L\)代表左,\(R\)代表右)的字典序应最小。
分析:
简单的(对大佬来说)计数DP。
我们设状态\(d[i][j][k]\)表示从坐标为\((i,j)\)的点到达最后一层的和为\(k\)的方案数。
因为它只能往左下或者右下走,所以我们不难得出状态转移方程为:
\[
d[i][j][k]=d[i+1][j][k-num[i][j]]+d[i+1][j+1][k-num[i][j]
\]
注意以上方程只针对于行数编号大于等于\(n\)的时候,因为我们发现在前\(n\)行中的转移应该是
\[
d[i][j][k]=d[i+1][j-1][k-num[i][j]]+d[i+1][j][k-num[i][j]]
\]
然后进行转移即可。(注:有些博客上把状态转移方程中的\(val\)((也即\(num[i][j]\))解释错了,可能是笔误)
Code:
/* Problem ID:UVa 10564 Author: dolires Date: 30/09/2019 09:16 */ #include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long using namespace std; const int maxn=25; ll d[maxn][maxn][510]; int n; int S; int num[maxn<<1][maxn]; template<class T>void read(T &x) { bool f=0;char ch=getchar();x=0; for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') f=1; for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0'; if(f) x=-x; } void print(int i,int j,int sum) { if(i>=2*n-1) return;//注意这里是等于时就要退出,因为在上一次递归时就已经输出了这一步的策 //略 if(i<n) { if(j>1&&d[i+1][j-1][sum-num[i][j]]) { printf("L");//这里就已经输出了下一步是往左走还是往右走 print(i+1,j-1,sum-num[i][j]); } else { printf("R"); print(i+1,j,sum-num[i][j]); } return; } else { if(d[i+1][j][num[i][j]]) { printf("L"); print(i+1,j,sum-num[i][j]); } else { printf("R"); print(i+1,j+1,sum-num[i][j]); } } } int main() { read(n);read(S); for(int i=1;i<=n;++i) { for(int j=1;j<=n-i+1;++j) { read(num[i][j]); } } for(int i=n+1;i<=2*n-1;++i) { for(int j=1;j<=i-n+1;++j) { read(num[i][j]); } } for(int i=1;i<=n;++i) { d[2*n-1][i][num[2*n-1][i]]=1; } for(int i=2*n-2;i>=n;--i) { for(int j=1;j<=i+1-n;++j) { for(int k=num[i][j];k<=S;++k) { d[i][j][k]=d[i+1][j][k-num[i][j]]+d[i+1][j+1][k-num[i][j]]; } } } ll ans=0; for(int i=n-1;i>=1;--i) { for(int j=1;j<=n-i+1;++j) { for(int k=num[i][j];k<=S;++k) { if(j>1) d[i][j][k]+=d[i+1][j-1][k-num[i][j]]; if(j<n-i+1) d[i][j][k]+=d[i+1][j][k-num[i][j]];//之所以加特判是因为在 //上半部分的时候,越上面列数越多 } if(i==1) ans+=d[1][j][S]; } } printf("%lld\n",ans); for(int i=1;i<=n;++i) { if(d[1][i][S]) { printf("%d ",i-1);//因为是从0开始编号的 print(1,i,S); break; } } printf("\n"); return 0; }
后序:
很多时候我看着网上题解放的代码看起来很长,所以不愿意去写,就比如说这一篇,但是当你真正理解了这一道题的时候,代码虽然看起来长,但是写起来都是很容易的。
11.Headmaster's Headache,UVa 10817
题意:
某校有\(n\)个教师和\(m\)个求职者。已知每人的工资和能教的课程集合,要求支付最少的工资使得每门课都至少有两名老师教学。在职教师必须招聘。
科目个数\(1<=s<=8\),在职教师人数\(1<=m<=20\)和申请者个数\(1<=n<=100\)。
分析:
在紫书上这是一道例题,而且当时我还熬夜看这道题的题解,看不懂就在一直看代码,所以印象比较深刻,应该可以自己试着把它打出来。
因为要使每门课都至少有两个老师教,那么每门课的状态就只有三种:
没有老师教,有一个老师教,有至少两个老师教。
再看看数据范围(数据范围很多时候往往就是会提醒你正解是什么),一看就知道是状态压缩动态规划嘛。
那我们现在就设\(d[i][s1][s2]\)表示前\(i\)个人,状态集合为\(S1\)的科目有\(1\)个老师教,状态集合为\(S2\)的科目有两个老师教的时候至少需要给的工资(多于2个老师教把它记在\(k=2\)的集合状态中)。
因为已知有一个老师教的科目和有两个老师教的科目,就可以直接计算出没有老师教的科目,所以没必要再设这一维的状态。
对于一个老师,如果他是在职教师,就只能够有一种决策,那就是选他,否则的话对于求职者,你可以选择选他或者是不选他。
然后通过他们的能教课程的集合来更新状态,详细的实现情况请关键代码。
Code:
/* Problem ID:UVa 10817 Author: dolires Date: 29/09/2019 22:22 */ #include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> using namespace std; const int maxn=10; const int inf=0x7fffffff; int d[200][1<<maxn][1<<maxn]; int teach[200]; int salary[200]; template<class T>void read(T &x) { bool f=0;char ch=getchar();x=0; for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') f=1; for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0'; if(f) x=-x; } int n,s,m; char c[maxn]; int dp(int i,int s0,int s1,int s2) { if(i==m+n+1) return s2==(1<<s)-1?0:inf; if(d[i][s1][s2]>=0) return d[i][s1][s2]; int &ans=d[i][s1][s2]; ans=inf; if(i>m) ans=dp(i+1,s0,s1,s2);//不聘用这位老师 int m0=s0&teach[i]; int m1=s1&teach[i]; s0^=m0; s1=(s1^m1)|m0; s2|=m1; ans=min(ans,salary[i]+dp(i+1,s0,s1,s2)); return ans; } int main() { read(s);read(m);read(n); for(int i=1;i<=m;++i) { cin>>c; for(int j=0;j<s;++j) { if(c[j]=='1') teach[i]|=(1<<j); } } for(int i=m+1;i<=m+n;++i) { cin>>c; for(int j=0;j<s;++j) { if(c[j]=='1') teach[i]|=(1<<j); } } memset(d,-1,sizeof(d)); printf("%d\n",dp(1,0,0,0)); return 0; }
注:这里的输入不太满足要求,我主要是因为想练一练状压DP的部分的写法,如果对于输入有要求的同学,可以上网参考一下别的大佬的博客。
11.Strategic Game,SEERC 2000,LA 2038
题意:
给定一棵树,选择尽量少的结点,使得每个没有选中的节点至少和一个已选结点相邻。
分析:
一道树形DP的基础题(太简单了)。
没什么好写的。
还是写一点吧。
如果不选这个节点,那么一定要选它的子结点,如果选了这个节点,可以选它的子结点也可以不选。
Code:
/* Problem ID:LA 2038 Author: dolires Date: 30/09/2019 09:49 */ #include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> using namespace std; const int maxn=1510; int d[maxn][3]; template<class T>void read(T &x) { bool f=0;char ch=getchar();x=0; for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') f=1; for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0'; if(f) x=-x; } int head[maxn],tot; bool vis[maxn]; struct Edge { int to,nxt; Edge(){}; Edge(int to,int nxt):to(to),nxt(nxt){}; }ed[maxn<<1]; void add(int u,int v) { ed[++tot]=Edge(v,head[u]); head[u]=tot; ed[++tot]=Edge(u,head[v]); head[v]=tot; } void dp(int u) { d[u][0]=0,d[u][1]=1; vis[u]=true; for(int i=head[u];i;i=ed[i].nxt) { int v=ed[i].to; if(vis[v]) continue; dp(v); d[u][0]+=d[v][1]; d[u][1]+=min(d[v][0],d[v][1]); } } int main() { int n; read(n); for(int i=1;i<n;++i) { int u,v; read(u);read(v); add(u,v);//其实都没必要用链式前向星存边的 } dp(1); printf("%d\n",min(d[1][0],d[1][1])); return 0; }
至此,我已经把蓝书上所说的动态规划基础题做完了。
所以完结撒花。
不,还有进阶版。
请移步动态规划(四)继续观看,