动态规划(三)

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\)代表右)的字典序应最小。

1569763076166

分析:

简单的(对大佬来说)计数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;
}

至此,我已经把蓝书上所说的动态规划基础题做完了。

所以完结撒花。

不,还有进阶版。

请移步动态规划(四)继续观看,

posted on 2019-11-09 22:15  dolires  阅读(118)  评论(0)    收藏  举报

导航