目录
C:POJ-1651 Multiplication Puzzle
两个定义(做题要用到):
定义1 对于图G=(V,E)来说, 最小支配集 指的是从V中取尽量少的点组成一个集合,使得对于V中剩余的点都与取出来的点有边相连。也就是说,设V‘是图G的一个支配集,则对于图中的任意一个顶点u,要么属于集合V’,要么与V‘中的顶点相邻。在V’中出去任何元素后V‘不再是支配集,则支配集是极小支配集。称G的所有支配集中顶点个数最少的支配集为最小支配集,最小支配集中顶点的个数称为支配数。
定义2 对于图G=(V,E)来说, 最小点覆盖 指的是从V中取尽量少的点组成一个集合,使得E中所有的边都与取出来的点相连。也就是说,设V‘是图G的一个顶点覆盖,则对于图中的任意一条边(u,v),要么u属于集合V’,要么v属于集合V‘。在V‘中除去任何元素后V’不在是顶点覆盖,则V‘是极小顶点覆盖。称G的所有顶点覆盖中顶点个数最少的覆盖为最小点覆盖。
区间DP(转自区间DP)
例题:石子归并。
描述 有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,并将新的一堆石子数记为该次合并的得分。
给组测试数据
输入 4 表示有4堆石子
4 4 5 9 表示每堆石子的个数
输出 54 表示最大的得分
分析:主要思想就是一个一个累加:4 4 5 9 先看下去是我想知道dp[i][j]的最大值,i表示起始位置,j表示终止位置,所以我肯定是想求出dp[1][4]间的最大值但是我从1到4可是如图这三种截取方法,所以我先从小的开始记录。
dp[1][1]=4;dp[2][2]=4;dp[3][3]=5;dp[4][4]=9。然后我在长度为2的时候记录,dp[1][2]=4+4=8,dp[2][3]=8+5=14;dp[3][4]=14+9=23;这样加起来的值就是8+14+23=45;但是如果我反过来呢?dp[1][2]=5+9=14;dp[2][3]=14+4=18;dp[3][4]=18+4=22;这样加起来的值就是14+18+22=54。很明显,54就是所求的最大值。
如图,如果我相求圈着的这个三个的值,我完全可以有图上这两种分割,并且分割出来的值是已经知道的了。
动态规划的思想:先两两合并,在三三合并,最后再NN合并,在合并过程中,较大的区间可以拆分成已经的小区间进行计算,省时又省力。比如,我可以在三三合并的时候可以拆分成一一还有二三相加。即把当前阶段的合并方法细分成前一阶段已计算出的方法,选择其中的最优方案。
具体来说我们应该定义一个数组dp[i,j]用来表示合并方法,i表示从编号为i的石头开始合并,j表示所求区间的结尾,sum表示的是石头的数量。
对于上面的例子来说,
第一阶段:dp[1][1],dp[2][2],dp[3][3],dp[4][4] 因为一开始还没有合并,所以这些值应该全部为0。
第二阶段:两两合并过程如下,其中sum(i,j)表示石头的数量,即从i开始数j个数的和
dp[1,2]=dp[1,1]+dp[2,2]+sum[1,2];
dp[2,3]=dp[2,2]+dp[3,3]+sum[2,3];
dp[3,4]=dp[3,3]+dp[4,4]+sum[4,4];
第三阶段:三三合并可以拆成两两合并,拆分方法有两种,前两个为一组或后两个为一组
dp[1,3]=dp[1,2]+dp[3,3]+sum[1,3]或dp[1,3]=dp[1,1]+dp[2,3]+sum[1,3];取其最优
dp[2,4]=dp[2,2]+dp[3,4]+sun[2,4]或dp[2,4]=dp[2,3]+dp[3,3]+sum[2,4];取其最优
第四阶段:四四合并的拆分方法用三种,同理求出三种分法的得分,取其最优即可。以后第五阶段、第六阶段依次类推,最后在第六阶段中找出最优答案即可。
动态方程为dp[i][j]=dp[i][k]+dp[k+1][j]+sum[i][j];
参考代码。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
//#define MAX 999999
int main ()
{
int dp[210][210],sum[210][210],a[210];
int n;
while(~scanf("%d",&n))
{
//memset(dp,MAX,sizeof(dp));
for (int i=1; i<=n; i++)
scanf("%d",&a[i]);
for (int i=1; i<=n; i++)
{
dp[i][i]=0;//初始化为0
sum[i][i]=a[i];//将每堆石子的个数赋值进来
}
for (int len=1; len<n; len++)//按长度从小到大枚举
{
for (int i=1; i<=n&&i+len<=n; i++)//i表示开始位置
{
int j=len+i; //j表示长度为len的一段区间的结束位置
for (int k=i; k<j; k++) //用k来表示分割区间
{
sum[i][j]=sum[i][k]+sum[k+1][j];
if (dp[i][j]<dp[i][k]+dp[k+1][j]+sum[i][j])
dp[i][j]=dp[i][k]+dp[k+1][j]+sum[i][j];
//cout<<i<<" "<<j<<" "<<sum[i][j]<<" "<<k<<" "<<dp[i][j]<<endl;
}
}
}
cout<<dp[1][n]<<endl;
}
return 0;
}
另一种:
一条直线上有N堆石子,现在要将所有石子合并成一堆,每次只能合并相邻的两堆,合并花费为新合成的一堆石子的数量,求最小的花费。
1堆,花费为0
2堆,花费为sum[2]
3堆,花费为min(a[1] + a[2], a[2] + a[3]) + sum[3]
如果我们有n堆,合并的最后一次操作一定是从两堆合并成一堆,
第一种模型就是将大区间从任意位置分成两个小区间
规定dp[i][j]为合并第i堆到第j堆的最小花费
DP方程为:
dp[i][j] = min(dp[i][k] + dp[k+1][j]) + sum[j] - sum[i-1] (i <= k < j) sum[i]为前i项和
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; i++)
dp[i][i] = 0;
for (int len = 2; len <= n; len++)
{
for (int i = 1, j = len; j <= n; i++, j++)
{
for (int k = i; k < j; k++)
{
if(dp[i][j] > dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1])
dp[i][j] = dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1];
}
}
}
printf("%d
", dp[1][n]);
树形DP
树形 DP,即在树上进行的 DP。树形DP具体还是要根据题来说,就根据E~H题来讲解。
例题
A:POJ-2955 Brackets:给一个括号组成的字符串,问最多能匹配多少个括号 像([)]这样的字符串匹配度为2,但是像()[]、[()]的字符串匹配度为4,也就是说括号具有分隔作用。长度为1的串匹配度 0 。长度为2的串匹配度 0 或 2。
规定dp[i][j]为合并第i到第j个字符的最大匹配度
长度为n时,我们可以先检测a[i]和a[j]是否匹配,
匹配dp[i][j] = dp[i+1][j-1] + 2
不匹配,那我们可以按第一种模型处理,从任意位置分成两个区间
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
int dp[105][105];
int main()
{
char str[105];
while(scanf("%s",str+1)!=EOF){
memset(dp,0,sizeof(dp));
if(str[1]=='e')
break;
int n=strlen(str+1);
for(int i=1;i<=n;i++)
dp[i][i]=0;
for(int len=1;len<=n;len++){
for(int i=1,j=len;j<=n;i++,j++){
if((str[i]=='('&&str[j]==')')||(str[i]=='['&&str[j]==']'))
dp[i][j]=dp[i+1][j-1]+2;
for(int k=i;k<j;k++)
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
}
}
cout<<dp[1][n]<<endl;
}
}
B:UVA-10003 Cutting Sticks:题目的意思是有个长度为l的木棍,这个木棍需要加工n次,给出n次加工的位置,每次加工的花费是当前木棍的长度,求加工的最小花费。
与矩阵连乘多少有点类似,不过这里不是枚举最后一次切割的位置,这里最后一次切割的花费和之前的切割次序是相关的。所以不能这样枚举,但我们发现第一次切割时的花费是固定的,可以尝试一下枚举第一次切割的位置。用dp[i][j]代表第i次切割到第j次切割的最优值,我们可以得到这一段的总长度是pos[j+1]-pos[i-1],这里注意边界,要特殊处理下,然后枚举这一段第一次切割的位置k,转移方程为dp[i][j]=min(dp[i][k-1]+dp[k+1][j]+pos[j+1]-pos[i-1]),其中k为从i到j。这里也要注意边界的问题,当i大于j的时候dp[i][j]应该为0,其他的情况都应该初始化为一个足够大的值。只要注意号边界就可以了。这里有两种写法,一种是记忆化搜索,另一种是递推。
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int INF=0x3f3f3f3f;
int dp[105][105];
int a[105];
int main()
{
int s;
while(scanf("%d",&s)&&s){
int n;
scanf("%d",&n);
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
a[0]=0;a[++n]=s;
for(int len=2;len<=n;len++){
for(int i=0,j=len;j<=n;i++,j++){
int tmp=INF;
for(int k=i+1;k<j;k++)
if(tmp>dp[i][k]+dp[k][j]+a[j]-a[i])
tmp=dp[i][k]+dp[k][j]+a[j]-a[i]; //a[j]-a[i]即转移要付出的代价
dp[i][j]=tmp;
}
}
printf("The minimum cutting is %d.
",dp[0][n]);
}
}
C:POJ-1651 Multiplication Puzzle:题意:从一堆放好的卡片中取一个卡片(不能取开头和结尾的卡片),每次取出一个卡片的时候,就计算一个值:该卡片上的数字和他左右卡片的数字的乘积。重复这个操作直到只剩下两个卡片,将所有得到的值加起来。求这个值的最小值是多少。 题意:dp[i][j]表示从第i张到第j张的最小的值,状态转移方程dp[i][j] = min(dp[i][j],dp[i][k] + dp[k][j] + a[i] * a[k] * a[j]),k表示最后一个取走的卡牌,i<k<j。
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
#define maxn 105
#define INF 0x3f3f3f3f
int main()
{
int dp[maxn][maxn];
int a[maxn];
int n;
while(cin>>n){
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
cin>>a[i];
for(int l=2;l<=n;l++){
for(int i=1,j=l+i;j<=n;j++,i++){
dp[i][j]=INF;
for(int k=i+1;k<=j-1;k++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[j]*a[k]);
}
}
}
cout<<dp[1][n]<<endl;
}
return 0;
}
D:HDU-3506 Monkey Party:(石子合并的升级版)题意: 一群猴子围成一圈,每个猴子有一个权值,然后每次选相邻的两个猴子交友,代价就是两个权值和,a和b交友之后呢,a的朋友和b的朋友就是朋友了,代价是a,b的朋友的总和,让你求最小代价。分析: 这个和经典的取石子问题及其相似,不同的是,这个可以围成一个圈,我们只要预处理下 n*2即可,然后用四边形不等式优化下,不懂的点这里,注意清零即可。(其实没必要明白优化原理,只需要知道使用条件和s数组的初始化和更新即可。
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=2e3+10;
#define INF 0x3f3f3f3f
int dp[maxn][maxn];
int a[maxn],s[maxn][maxn],sum[maxn];
int n;
int main()
{
while(cin>>n){
sum[0]=0;
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
for(int i=0;i<=2*n;i++)
s[i][i]=i;
for(int i=n+1;i<=2*n;i++)
sum[i]=sum[n]+sum[i-n];
for(int l=2;l<=n;l++){
for(int i=1,j=l+i-1;j<=2*n;j++,i++){
dp[i][j]=INF;
for(int k=s[i][j-1];k<=s[i+1][j];k++){
if(dp[i][j]>dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]){
dp[i][j]=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1];
s[i][j]=k;
}
}
}
}
int ans=INF;
for(int i=1;i<=n;i++)
ans=min(ans,dp[i][i+n-1]);
cout<<ans<<endl;
}
return 0;
}
E:POJ-2342 Anniversary party:题意:某公司要举办一次晚会,但是为了使得晚会的气氛更加活跃,每个参加晚会的人都不希望在晚会中见到他的直接上司,现在已知每个人的活跃指数和上司关系(当然不可能存在环),求邀请哪些人(多少人)来能使得晚会的总活跃指数最大。题解:树形dp.dp[node][1] += dp[i][0]; i是node的下属,1表示上司(node)去,0表示下属(i)不去dp[node][0] += max(dp[i][1],dp[i][0]);这个题看代码应该就能看明白。
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=6e3+10;
int dp[maxn][2];
int father[maxn],vis[maxn];
int n;
void treedp(int node)
{
vis[node]=1;
for(int i=1;i<=n;i++){
if(!vis[i]&&father[i]==node){
treedp(i);
dp[node][1]+=dp[i][0]; //上司来,下属不来
dp[node][0]+=max(dp[i][0],dp[i][1]); //上司不来,下属来不来都可以
}
}
}
int main()
{
int f,c,root;
while(scanf("%d",&n)==1){
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
memset(father,0,sizeof(father));
for(int i=1;i<=n;i++)
scanf("%d",&dp[i][1]);
root=0;
while(scanf("%d %d",&c,&f)&&c&&f){
father[c]=f;
root=f;
}
while(father[root])
root=father[root];
treedp(root);
printf("%d
",max(dp[root][0], dp[root][1]));
}
}
F:HDU-1054 Strategic Game:(最小顶点覆盖问题)题意:一棵树,要放置哨兵,要求最少放置多少哨兵能监视到所有的结点。
解题思路:放置哨兵无非两种情况,要嘛放,要嘛不放,我们可以用dp[i][1]来表示第i个结点放置哨兵,dp[i][0]来表示第i个结点不放置哨兵,我们可以从上往下,从左往右来遍历树,所以这就用到了树形DP的知识,我们很容易知道,如果父亲结点没放哨兵,那么子结点肯定要放置哨兵,如果父亲放置了哨兵,那么子结点可以考虑放或者不放。所以很容易写出状态转移方程dp[v][1] += min(dp[u][1],dp[u][0]),dp[v][0] += dp[u][1],由于子结点可能有多个,我们要依次从左到右遍历所以我们必须开一个brother来存放它前一个兄弟结点,然后只要DFS递归的来从上往下遍历就可以得解了。(外附二分图匹配做法)
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=6e3+10;
int dp[maxn][2];
int father[maxn],son[maxn],brother[maxn];
int n;
void treedp(int root)
{
dp[root][0]=0;
dp[root][1]=1;
int f=son[root];
while(f!=-1){
treedp(f);//纵向遍历
dp[root][1]+=min(dp[f][0],dp[f][1]);
dp[root][0]+=dp[f][1];
f=brother[f];//切换到下一个兄弟结点,横向遍历
}
}
int main()
{
int m,root,num,cnt,num1;
while(scanf("%d",&m)==1){
memset(son,-1,sizeof(son));
memset(father,0,sizeof(father));
for(int i=0;i<m;i++){
scanf("%d:(%d)",&num,&cnt);
for(int j=0;j<cnt;j++)
{
cin>>num1;
brother[num1]=son[num];
son[num]=num1;
father[num1]=1;
}
}
for(int i=0;i<m;i++){
if(!father[i]){
root=i;
break;
}
}
treedp(root);
printf("%d
",min(dp[root][0], dp[root][1]));
}
}
二分图匹配做法
#include <bits/stdc++.h>
using namespace std;
const int maxn=1500+7;
int n;
vector<int> G[maxn];
int nxt[maxn],visited[maxn],cnt;
int find(int x){
for(int i = 0;i<G[x].size();++i){
int v=G[x][i];
if(!visited[v]){
visited[v]=1;
if(nxt[v]==-1||find(nxt[v])){
nxt[v]=x;
return 1;
}
}
}
return 0;
}
int match(){
int ans=0;
memset(nxt,-1,sizeof(nxt));
for(int i = 0;i<n;++i){
memset(visited,0,sizeof(visited));
if(find(i)) ans++;
}
return ans;
}
int main(int argc, char** argv) {
while(~scanf("%d",&n)){
for(int i = 0;i<=1500;++i) G[i].clear();
for(int i = 0;i<n;++i){
int x,y,num;
scanf("%d:(%d)",&x,&num);
if(!num) continue;
while(num--){
scanf("%d",&y);
G[x].push_back(y);
G[y].push_back(x);
}
}
printf("%d
",match()/2);
}
return 0;
}
G:ZOJ-3201 Tree of Tree:题意:有一个n个点的树,每个点有个权值,问大小为k的子树的最大权值和为多少。题解:设dp[x][j]为x点大小为j的子树的最大权值和,则dp[x][j] = max(dp[x][j], dp[x][k] + dp[v][j - k]);任选一点为根往下搜索即可!
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN=105;
vector<int> tree[MAXN];
int n,k;
int dp[MAXN][MAXN]; //dp[i][k]表示以i为根节点的最大的k个节点的和
int vis[MAXN];
int maxn;
void dfs(int u)
{
vis[u]=1;
for(int i=0;i<tree[u].size();i++)
{
int v=tree[u][i];
if(!vis[v])
{
dfs(v);
for(int j=k;j>=1;j--) //每个根节点不仅要找出k个节点和的最大值,还要算出1到k-1的 对应的节点和的最大值,是为了给后面的根节点用
for(int m=1;m<j;m++)
dp[u][j]=max(dp[u][j],dp[u][m]+dp[v][j-m]); //根节点的m个和 加上 儿子的j-m个和 一共j个和 不断地更新dp[u][j]
}
}
}
int main()
{
while(scanf("%d%d",&n,&k)!=EOF)
{
maxn=0;
for(int i=0;i<n;i++) //每次一定不要忘记将树清空!!!
tree[i].clear();
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
for(int i=0;i<n;i++)
scanf("%d",&dp[i][1]);
for(int i=0;i<n-1;i++)
{
int u,v;
scanf("%d%d",&u,&v);
tree[u].push_back(v);
tree[v].push_back(u);
}
dfs(0);
for(int i=0;i<n;i++)
{
if(dp[i][k]>maxn) maxn=dp[i][k];
}
printf("%d
",maxn);
}
return 0;
}
H:POJ-3659 Cell Phone Network:(最小支配集问题)有一棵树,问最少取树中的多少个节点,能使其与树的其他节点都联通。(一个点只与其有边相连的点联通)。具体题解可看代码中的注释。
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define MAXN 10010
#define INF 0x3f3f3f3f
vector <int> G[MAXN];
int dp[MAXN][MAXN]; //dp[i][0]表示取i结点时的最小结点数
//dp[i][1]表示不取i结点时,i被其儿子覆盖时的最小结点数
//dp[i][2]表示不选点i,i被其父亲覆盖时的最小结点数
//树形DP状态转移从叶子结点开始向根不断转移,所以用深搜。
//搜到叶子结点就结束。
//转移的时候可以将结点以下给圈起来。
//取与不取,树上背包
int vis[MAXN];
void dfs(int u)
{
vis[u]=1;
int i;
int flag=1,tmp=INF;
for(i=0;i<G[u].size();i++)
{
int v=G[u][i];
if(!vis[v]) //额,图的最小支配集吧
{
dfs(v);
dp[u][0]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
dp[u][2]+=min(dp[v][0],dp[v][1]); //不取的话,肯定没有父亲这个选项啊
//父亲只有一个,所以不必做过多的讨论
//儿子比较多,可以取所有儿子,但必须选一个儿子
//但选了一个儿子就要做标记
if(dp[v][0]<=dp[v][1])
{
dp[u][1]+=dp[v][0];
flag=0;
}
else
{
dp[u][1]+=dp[v][1];
tmp=min(tmp,dp[v][0]-dp[v][1]);
}
}
}
if(flag) //还没有选儿子,加上这个差值,补回一个儿子
dp[u][1]+=tmp;
return ;
}
int main()
{
int n;
int i=0;
int u,v;
scanf("%d",&n);
for(i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
G[u].push_back(v);
G[v].push_back(u);
}
for(i=0;i<=n;i++) //dp[i][0]的i从0开始,因为存在下标为0的结点
{ //再次强调结点的下标一定是0~n-1
dp[i][0]=1;
dp[i][1]=0;
dp[i][2]=0;
}
if(n==1)
printf("1
");
else
{
dfs(1);
printf("%d
",min(dp[1][0],dp[1][1]));
}
return 0;
}