杭电60道DP问题总结:
DP是一个很有艺术的思想。看似简单的背后却隐藏着深刻的含义。
题目连接地址:http://acm.hdu.edu.cn/problemclass.php?id=516&page=1
学习算法最好的办法就是不断的练习。
本来想等把全部的DP问题做完了再发一个总帖,但是发现还是会有忘记,断断续续的做了一些,还是先总结一部分,之后的等考完试了再补充回来。
所有题目都AC过了,有自己写的,也有从网上参考学习的,拿出来与大家交流分享,我个人对某些题目的理解可能有误,如果有的话,还希望大家批评指正。
言归正传,一道一道题目来。
ProID 1003 Max Sum
题目意思是,给定一个数组,然后求这个数组中最大的连续子串之和,输出不仅需要输出最大值,还需要输出这个范围。
这算是最简单的DP问题之一了。
设 f[x] 为以 a[x]终止且包含 a[x] 的最大序列的和,有:f[1] = a[1];
f[x+1] = f[x] > 0 ? f[x] + a[x+1] : a[x+1],那么最大子序列的和就是 f[1] .. f[n] 中最大的一个。
这里一定要注意是以a[x]结尾的,就是所谓的无后效性,开始学习的时候不明白为什么一定要以a[x]结尾并且包含a[x]呢,注意DP是把大问题的规模缩小,现在我们假设f[x]为仅仅以a[x]结尾但不包括a[x]的最大子串之和,那么扩大问题的公式(就是所谓的递归方程式或者状态转移方程)如何写呢?
f[x+1]=?无法推导了,因为当考虑第x+1元素时,题中要求是连续子串,f[x]表示的前x个中的子串,可不一定包括第x个,虽然无后效性了,但是无法从前边的状态推导过来,即只能解决规模为1的问题。
一定记住,状态转移方程是从当前状态转到下一个状态时,增大问题规模,但是一定是与前一个状态而来的,切记。
回到问题上来,就可以理解为什么以a[x]结尾并且包含a[x]了。
至于找到区间就简单了,首先在结果集中找到最大值,注意f[x]表示包含第x个元素,但是最终的结果可不一定以最后一个元素结尾,所以还需要对f数组搜索,找到最大值,然后记录下最大值的位置,向前依次减去元素,等到结果为0时在记录下此时的下表,这样就出结果了。
参考代码如下:
[cpp] view plain copy print ?
- #include<cstdio>
- #include<cstring>
- #include<cstdlib>
- int a[100001];
- int dp[100001];
- int main()
- {
- int T;
- scanf("%d",&T);
-
- for(int i=0;i<T;i++)
- {
- int n;
- scanf("%d",&n);
-
-
-
-
-
- memset(dp,0,sizeof(dp));
- for(int j=0;j<n;j++)
- scanf("%d",&a[j]);
- dp[0]=a[0];
- for(int k=1;k<n;k++)
- {
- if(dp[k-1]>0)
- dp[k]=dp[k-1]+a[k];
- else
- dp[k]=a[k];
- }
- int start=0;
- int max=dp[0];
- for(int k=1;k<n;k++)
- {
- if(max<dp[k])
- {
- start=k;
- max=dp[k];
- }
- }
- int end=start;
- for(int k=end-1;k>=0;k--)
- {
- if(dp[k]>=0)
- end=k;
- else
- break;
- }
-
-
- printf("Case %d:\n",i+1);
- printf("%d %d %d\n",max,end+1,start+1);
- if(i!=T-1)
- printf("\n");
- }
-
-
- return 0;
- }
#include<cstdio>
#include<cstring>
#include<cstdlib>
int a[100001];
int dp[100001];
int main()
{
int T;
scanf("%d",&T);
for(int i=0;i<T;i++)
{
int n;
scanf("%d",&n);
/*
设 f[x] 为以 a[x] 终止且包含 a[x] 的最大序列的和,有:f[1] = a[1];
f[x+1] = f[x] > 0 ? f[x] + a[x+1] : a[x+1]
那么最大子序列的和就是 f[1] .. f[n] 中最大的一个。
*/
memset(dp,0,sizeof(dp));
for(int j=0;j<n;j++)
scanf("%d",&a[j]);
dp[0]=a[0];
for(int k=1;k<n;k++)
{
if(dp[k-1]>0)
dp[k]=dp[k-1]+a[k];
else
dp[k]=a[k];
}
int start=0;
int max=dp[0];
for(int k=1;k<n;k++)
{
if(max<dp[k])
{
start=k;
max=dp[k];
}
}
int end=start;
for(int k=end-1;k>=0;k--)
{
if(dp[k]>=0)
end=k;
else
break;
}
printf("Case %d:\n",i+1);
printf("%d %d %d\n",max,end+1,start+1);
if(i!=T-1)
printf("\n");
}
return 0;
}
ProID 1011 Starship Troopers(这个电影倒是很不错的)
这个题目还是有点难度的,我也是在网上查了些资料才大概搞明白的,树形DP问题。
大概意思是:有N个洞,M个士兵,每个洞中有a个bug,b个brain,每个士兵可以最多Kill掉20个bug,入口在洞口1处,问可以得到多少个brain。
这些洞是按照图的形式连接的,题中在输入数据的时候给出了图的边。不过根据题意,只有N-1条边,所以刚好是一个树,也就是所谓的树形DP,当然我是这样理解的,树形DP可不是这么简单,如果要深入理解的话,还是要去google一下资料(插一句,这方面的东西google出来的比baidu的多)。
回到题目,状态方程:value[a][b] = max(value[a][b],value[a][b - k] + value[j][k]),其中a、j之间有边相连。value[a][b]表示以a为根节点的子树,包含b个士兵,所能得到最大brain。 按a来分层进行动态规划。 这里的状态转移方程是根据与之相连的点所决定的,因为只能从当前节点走到与之相连的点。注释在代码中写的很清楚了。
代码如下:
[cpp] view plain copy print ?
- #include<iostream>
- #include<cstdio>
- #include<cstring>
- #include<algorithm>
- using namespace std;
- struct Room
- {
- int bugs;
- int brains;
- };
- Room rooms[101];
- int value[101][101];
- int matrix[101][101];
- int visited[101];
- int n,m;
- void dpTree(int u)
- {
- visited[u]=1;
- int r=(rooms[u].bugs+19)/20;
- for(int i=m;i>=r;i--)
- value[u][i]=rooms[u].brains;
- for(int v=1;v<=n;v++)
- {
- if(matrix[u][v] && !visited[v])
- {
- dpTree(v);
- for(int j=m;j>=r;j--)
- for(int k=1;k<=j-r;k++)
- value[u][j]=max(value[u][j],value[u][j-k]+value[v][k]);
- }
- }
- }
- int main()
- {
- while(scanf("%d %d",&n,&m)!=EOF)
- {
- if(n==-1)
- break;
- memset(rooms,0,sizeof(rooms));
- memset(matrix,0,sizeof(matrix));
- memset(visited,0,sizeof(visited));
- memset(value,0,sizeof(value));
- for(int i=1;i<=n;i++)
- scanf("%d %d",&rooms[i].bugs,&rooms[i].brains);
- for(int i=1;i<n;i++)
- {
- int u,v;
- scanf("%d %d",&u,&v);
- matrix[u][v]=matrix[v][u]=1;
- }
- if(m==0)
- {
- printf("0\n");
- continue;
- }
- dpTree(1);
- printf("%d\n",value[1][m]);
- }
- return 0;
- }
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct Room
{
int bugs;
int brains;
};
Room rooms[101];
int value[101][101];
int matrix[101][101];
int visited[101];
int n,m;
void dpTree(int u)
{
visited[u]=1;
int r=(rooms[u].bugs+19)/20; //这个r表示当前这个房间里需要留几个士兵,即使是1个虫子也需要一个兵。
for(int i=m;i>=r;i--)
value[u][i]=rooms[u].brains;//只要在这个房子里留够士兵,那么就可以获得全部的brains
for(int v=1;v<=n;v++)
{
if(matrix[u][v] && !visited[v])//选择当前点相邻的点,并且此边没有访问过,注意只能往下走而不能走回去
{
dpTree(v);
for(int j=m;j>=r;j--)
for(int k=1;k<=j-r;k++)
value[u][j]=max(value[u][j],value[u][j-k]+value[v][k]);
}//这个状态转移方程的意思是,第u个房间里留j个士兵所获得brains是由在第u个房间里留j-k个士兵,在与之相连的第v个房间里就k个士兵所获得的brains所决定的。一定记住了,当前的状态仅仅由与其相连的点的状态所决定!!!
}
}
int main()
{
while(scanf("%d %d",&n,&m)!=EOF)
{
if(n==-1)
break;
memset(rooms,0,sizeof(rooms));
memset(matrix,0,sizeof(matrix));
memset(visited,0,sizeof(visited));
memset(value,0,sizeof(value));
for(int i=1;i<=n;i++)
scanf("%d %d",&rooms[i].bugs,&rooms[i].brains);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d %d",&u,&v);
matrix[u][v]=matrix[v][u]=1;
}
if(m==0)
{
printf("0\n");
continue;
}
dpTree(1);
printf("%d\n",value[1][m]);
}
return 0;
}
ProID 1024 Max Sum Plus Plus
给题目意思是,定由n个整数(可能为负整数)组成的序列e1,e2,…,en,以及一个正整数m,要求确定序列的m个不想交子段,使得这m个子段的总和达到最大。说实话,这个题目也是搞了好久才明白过来。
如果m=1,那么就是我们上边所说的第一道题目了,不过问题可不是那么简单的。分段之后可能原来不需要的元素变得可以使用了,所以得转换一下思路。
根据一般的思路而言,状态的转移也就是增大数据规模的过程,对于本题而言,就是不断考虑下一个元素的过程(也就是顺序DP),这里可以考虑使用模糊方程:
dp[i][j] =MAX(dp[?][?],dp[??][??])
套公式,dp[i][j]表示前j个元素分成i段的最大和(还记得我们第一题的内容么,是一个一维数组),那么考虑下一元素(注意这里不需要一定包含第j个元素了,因为有分段的存在)。
dp[i][j]=MAX(dp[i][j-1] , dp[i-1][k])+data[j] 其中i<k<=j。
表示第j个元素可以自成一段,或者拼接到第i段的最后一位。好了,问题解决了,不过回过头来看看,方程式中仅仅用到了当前状态和前一状态,所以不用开2维数组,根据题目要求,这个空间消耗比较大,只需要2个一维数组记录当前和原始状态就可以了。具体解释在代码中。
代码如下:
[cpp] view plain copy print ?
- #include<cstdio>
- #include<cstring>
- #include<algorithm>
-
- using namespace std;
- int curr_best[1000010];
- int prev_best[1000010];
- int max_sum,i,j;
- int n,m;
- int data[1000010];
- #define MIN_SUM 0x80000000
- int main()
- {
- while(scanf("%d %d",&m,&n)!=EOF)
- {
- for(int i=1;i<=n;i++)
- scanf("%d",&data[i]);
- memset(curr_best,0,sizeof(curr_best));
- memset(prev_best,0,sizeof(prev_best));
- int j=0;
- for(int i=1;i<=m;++i)
- {
- max_sum=MIN_SUM;
- for( j=i;j<=n;++j)
- {
-
-
- curr_best[j]=max(curr_best[j-1],prev_best[j-1])+data[j];
- prev_best[j-1]=max_sum;
- max_sum=max(max_sum,curr_best[j]);
- }
- prev_best[j-1]=max_sum;
- }
- printf("%d\n",max_sum);
- }
- return 0;
- }
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int curr_best[1000010];//保存当前状态
int prev_best[1000010];
int max_sum,i,j;
int n,m;
int data[1000010];
#define MIN_SUM 0x80000000
int main()
{
while(scanf("%d %d",&m,&n)!=EOF)
{
for(int i=1;i<=n;i++)
scanf("%d",&data[i]);
memset(curr_best,0,sizeof(curr_best));
memset(prev_best,0,sizeof(prev_best));
int j=0;
for(int i=1;i<=m;++i)
{
max_sum=MIN_SUM;
for( j=i;j<=n;++j)
{
//curr b(i,*);
//prev b(i-1,*);
curr_best[j]=max(curr_best[j-1],prev_best[j-1])+data[j];
prev_best[j-1]=max_sum;//这两条语句不能写反了,这块我纠结了好久,解释一下,prev_best[j-1]表示的是上一个状态中i...j-1的最大值,max_sum更新之后表示的i...j的最大值,所以不能写反了
max_sum=max(max_sum,curr_best[j]);
}
prev_best[j-1]=max_sum;//prev_best[j-1]中始终保持着前一个状态的最大值,这个很重要
}
printf("%d\n",max_sum);
}
return 0;
}
ProID 1025 Constructing Roads In JGShining's Kingdom
题目意思是,两条线,每条线上各有n个点,表示n个城市,第一条表示rich 城市,第二天表示poor 城市,从第一条线连接到第二条线上,问最多能练几条,已知所有可连接的线段。
开始没搞明白,才发现就是个最长上升子序列问题。不过用一般的解法会超时,查阅资料后方得知原来需要DP+二分查找。说来惭愧,当时看明白了,可惜今天再拿出来瞅瞅又给忘了。其实最终的代码很简单,但往往是越简单的代码蕴含的思想越是复杂牛逼。下面的解释摘自Lce_Crazy的博客。
假定存在一个序列d[1...9]=2 1 5 3 6 4 8 9 7,可以看出LIS长度为5。现在开始一步一步的找出来最终的结果。
定义序列B,令i=1...9来逐个考察。此外用变量len来记录现在最长算到了多少了。
首先,把d[1]放到B里,即B[1]=d[1],就是说长度为1的LIS的最小末尾是2,这时len=1,然后把d[2]有序的放到B里,令B[1]=1(d[2]),就是说长度为1的LIS最小末尾是1,d[1]=2已经没有用了,这是len=1不变。接着d[3]=5,5>1,所以令B[1+1]=d[3]=5,就是说长度为2的LIS的最小末尾是5,此时B[1...2]=1,5,len=2。
再来,d[4]=3,它正好在1和5之间,当然放在1的位置上不合适,因为1<3,因此长度为2的最小末尾应该是3,淘汰掉5,这时候B[1...2]=1,3,len=2,继续d[5]=6,它在3后边,所以B[3]=4,B[1...3]=1,3,6,len=3。
第6个,d[6]=4,它加在3和6之间,所以我们把6换掉,这样B[1...3]=1,3,4,len=3。
第7个,d[7]=8,8>4,所以B[4]=8,B[1...4]=1,3,4,8,len=4。
第8个,d[8]=9,B[1...5]=1,3,4,8,9,len=5。
最后一个,d[9]=7,所以B[1...5]=1,3,4,7,9,len=5。
注意这里的B并不是LIS,而是对应长度的LIS的最小末尾。
现在可以发现B插入数据是有序的,而且进行替换而不需要移动,也就是说可以使用二分查找,时间复杂度就降下来了。
PS:注意了,这种算法只能得到最终的数据个数,但是如果需要得到具体的内容就不行了。
提示一下,看网上说首先要排序,其实仔细看看题目的说明,它的边的输入是有序的,所以根本不需要进行排序。
代码如下:
[cpp] view plain copy print ?
- #include <iostream>
- #include <cstdio>
- #include <cstring>
- #include <algorithm>
- using namespace std;
- int num[50001];
- int ans[50001];
- int main()
- {
- int n,k=1;
- int len,low,heigh,mid;
- int tmp1,tmp2;
- while(scanf("%d",&n)!=EOF)
- {
- for(int i=0;i<n;i++)
- {
- scanf("%d %d",&tmp1,&tmp2);
- num[tmp1]=tmp2;
- }
- memset(ans,0,sizeof(ans));
- ans[1]=num[1];
- len=1;
- for(int i=2;i<=n;i++)
- {
- low=1;
- heigh=len;
- while(low<=heigh)
- {
- mid=(low+heigh)/2;
- if(ans[mid]<num[i])
- low=mid+1;
- else
- heigh=mid-1;
- }
- ans[low]=num[i];
- if(low>len)
- len++;
- }
- printf("Case %d:\n",k);
- if(len==1)
- printf("My king, at most 1 road can be built.");
- else
- printf("My king, at most %d roads can be built.",len);
-
- printf("\n\n");
- k++;
- }
- return 0;
- }
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int num[50001];
int ans[50001];
int main()
{
int n,k=1;
int len,low,heigh,mid;
int tmp1,tmp2;
while(scanf("%d",&n)!=EOF)
{
for(int i=0;i<n;i++)
{
scanf("%d %d",&tmp1,&tmp2);
num[tmp1]=tmp2;
}
memset(ans,0,sizeof(ans));
ans[1]=num[1];
len=1;
for(int i=2;i<=n;i++)
{
low=1;
heigh=len;
while(low<=heigh)
{
mid=(low+heigh)/2;
if(ans[mid]<num[i])
low=mid+1;
else
heigh=mid-1;
}
ans[low]=num[i];
if(low>len)
len++;
}
printf("Case %d:\n",k);
if(len==1)
printf("My king, at most 1 road can be built.");
else
printf("My king, at most %d roads can be built.",len);
printf("\n\n");
k++;
}
return 0;
}
ProID 1058 Humble Number
先呵呵下,这道题被各种吐槽,英文不过关啊。
题目是让找出第N个Humble Number,定义是除1外所有仅有2,3,5,7因子的数。
不多少了,这个题目或许可以用DP来解,但是貌似完全没有必要。不解释了。
代码如下:
[cpp] view plain copy print ?
- #include <iostream>
- #include <cstdio>
- #include <cstring>
- #include <algorithm>
- using namespace std;
-
- int n;
- typedef long long LL;
- LL data[5843];
-
- void init()
- {
-
- int i=1,j=1,m=1,n=1;
- int index=1;
- data[index]=1;
- while(1)
- {
- if(index==5843)
- break;
- LL tmp1=min(data[i]*2,data[j]*3);
- LL tmp2=min(data[m]*5,data[n]*7);
- LL res=min(tmp1,tmp2);
- if(res== data[i]*2) i++;
- else if(res== data[j]*3) j++;
- else if(res== data[m]*5) m++;
- else if(res== data[n]*7) n++;
-
- if(res!= data[index])
- data[++index]=res;
- }
- }
- int main()
- {
- init();
- while(scanf("%d",&n)!=EOF,n)
- {
- if(n%10==1 && n%100!=11)
- printf("The %dst humble number is %lld.\n",n,data[n]);
- else if(n%10==2 && n%100!=12)
- printf("The %dnd humble number is %lld.\n",n,data[n]);
- else if(n%10==3 && n%100!=13)
- printf("The %drd humble number is %lld.\n",n,data[n]);
- else
- printf("The %dth humble number is %lld.\n",n,data[n]);
- }
- return 0;
- }
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
typedef long long LL;
LL data[5843];
void init()
{
int i=1,j=1,m=1,n=1;
int index=1;
data[index]=1;
while(1)
{
if(index==5843)
break;
LL tmp1=min(data[i]*2,data[j]*3);
LL tmp2=min(data[m]*5,data[n]*7);
LL res=min(tmp1,tmp2);
if(res== data[i]*2) i++;
else if(res== data[j]*3) j++;
else if(res== data[m]*5) m++;
else if(res== data[n]*7) n++;
if(res!= data[index])
data[++index]=res;
}
}
int main()
{
init();
while(scanf("%d",&n)!=EOF,n)
{
if(n%10==1 && n%100!=11)
printf("The %dst humble number is %lld.\n",n,data[n]);
else if(n%10==2 && n%100!=12)
printf("The %dnd humble number is %lld.\n",n,data[n]);
else if(n%10==3 && n%100!=13)
printf("The %drd humble number is %lld.\n",n,data[n]);
else
printf("The %dth humble number is %lld.\n",n,data[n]);
}
return 0;
}
ProID 1059 Dividing
题目意思是,有一堆钻石,有6种不同的价值(1...6),现在告诉你每种的数量,问你能不能把这堆钻石平均按总的价值平均分开。
之前有做过9度上的一道题目,是把一堆球平均分成2堆,大概算法是:
求出所有总重量<= N / 2的可能性,如F[w]=true,表示可以累积1个或若干个球得到w的总重量。最后找出最接近N/2的即可。时间复杂度O(n*N),n为球的总数量,N为球的总质量。
这里有不同的地方,首先每个球的个数不定,而且我用上述方法的时候超时了,所以考虑别的方法。但有一点是一样了,只要能够得到刚好总价值一半的值,那么就可以平均分,怎么得呢,当然是背包问题了,不过不是01背包,而是多重背包问题,只要以总价值的一半为背包重量,那么只要能装满,就可以平均分。
代码是参考背包九讲中的公式:
[cpp] view plain copy print ?
- #include <iostream>
- #include <cstdio>
- #include <cstring>
- #include <algorithm>
- using namespace std;
-
- int data[7];
- int dp[500001];
- int m;
-
- void ZeroOnePack(int value,int weight)
- {
- for(int i=m;i>=value;i--)
- dp[i]=max(dp[i-value]+weight,dp[i]);
- }
- void CompletePack(int value,int weight)
- {
- for(int i=value;i<=m;i++)
- dp[i]=max(dp[i],dp[i-value]+weight);
- }
- void MultiPack(int value,int weight,int amount)
- {
- if(value*amount>=m)
- {
- CompletePack(value,weight);
- return ;
- }
- for(int k=1;k<amount;k*=2)
- {
- ZeroOnePack(k*value,k*weight);
- amount-=k;
- }
- ZeroOnePack(amount*value,amount*weight);
- }
- int main()
- {
- int count=1;
- while(scanf("%d",&data[1])!=EOF)
- {
-
- for(int i=2;i<=6;i++)
- scanf("%d",&data[i]);
- bool out=true;
- int sum=0;
- for(int i=1;i<=6;i++)
- {
- sum+=data[i]*i;
- if(data[i])
- out=false;
- }
- if(out)
- break;
- printf("Collection #%d:\n",count++);
- if((sum&1) ==1)
- {
- printf("Can't be divided.\n");
- printf("\n");
- continue;
- }
- m=sum/2;
- int value=0;
- memset(dp,0,sizeof(dp));
- for(int i=1;i<=6;i++)
- if(data[i])
- MultiPack(i,i,data[i]);
- if(dp[m]==m)
- printf("Can be divided.\n");
- else
- printf("Can't be divided.\n");
- printf("\n");
- }
- return 0;
- }
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int data[7];
int dp[500001];
int m;
void ZeroOnePack(int value,int weight)
{
for(int i=m;i>=value;i--)
dp[i]=max(dp[i-value]+weight,dp[i]);
}
void CompletePack(int value,int weight)
{
for(int i=value;i<=m;i++)
dp[i]=max(dp[i],dp[i-value]+weight);
}
void MultiPack(int value,int weight,int amount)
{
if(value*amount>=m)
{
CompletePack(value,weight);
return ;
}
for(int k=1;k<amount;k*=2)
{
ZeroOnePack(k*value,k*weight);
amount-=k;
}
ZeroOnePack(amount*value,amount*weight);
}
int main()
{
int count=1;
while(scanf("%d",&data[1])!=EOF)
{
for(int i=2;i<=6;i++)
scanf("%d",&data[i]);
bool out=true;
int sum=0;
for(int i=1;i<=6;i++)
{
sum+=data[i]*i;
if(data[i])
out=false;
}
if(out)
break;
printf("Collection #%d:\n",count++);
if((sum&1) ==1)
{
printf("Can't be divided.\n");
printf("\n");
continue;
}
m=sum/2;
int value=0;
memset(dp,0,sizeof(dp));
for(int i=1;i<=6;i++)
if(data[i])
MultiPack(i,i,data[i]);
if(dp[m]==m)
printf("Can be divided.\n");
else
printf("Can't be divided.\n");
printf("\n");
}
return 0;
}
ProID 1069
Monkey and Banana
题目大意是,一只猴子,想吃香蕉,给他一堆垫子,他把垫子堆起来自己站上去好能够到香蕉,堆垫子的规则是下边的面积一定要大于上边的面积。
这个题目依然是最长上升子序列问题,只不过比较的规则是面积而已。
PS:每种垫子可以使用多个,垫子有长宽高,可以以多种样子摆放。
不多解释了,代码如下:
[cpp] view plain copy print ?
- #include<iostream>
- #include<cstdio>
- #include<cstring>
- #include<algorithm>
- using namespace std;
- struct _data
- {
- int x,y,z,area;
- };
- _data data[200];
-
- bool com(_data d1,_data d2)
- {
- return d1.area<d2.area;
- }
- int n;
- int dp[200];
- int main()
- {
- int count=1;
- while(scanf("%d",&n)!=EOF&&n)
- {
- int index=0;
- for(int i=0;i<n;i++)
- {
- int x,y,z;
- scanf("%d %d %d",&x,&y,&z);
- data[index].x=x;data[index].y=y;data[index].z=z;data[index++].area=x*y;
- data[index].x=y;data[index].y=x;data[index].z=z;data[index++].area=x*y;
- data[index].x=y;data[index].y=z;data[index].z=x;data[index++].area=z*y;
- data[index].x=z;data[index].y=y;data[index].z=x;data[index++].area=z*y;
- data[index].x=x;data[index].y=z;data[index].z=y;data[index++].area=x*z;
- data[index].x=z;data[index].y=x;data[index].z=y;data[index++].area=x*z;
- }
- sort(data,data+index,com);
- memset(dp,0,sizeof(dp));
- dp[0]=data[0].z;
- for(int i=1;i<index;i++)
- {
- int temp=0;
- for(int j=0;j<i;j++)
- if(data[i].x>data[j].x && data[i].y>data[j].y&&temp<dp[j])
- temp=dp[j];
- dp[i]=temp+data[i].z;
- }
- int ans=dp[0];
- for(int i=1;i<index;i++)
- ans=max(ans,dp[i]);
- printf("Case %d: maximum height = %d\n",count++,ans);
- }
- return 0;
- }
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct _data
{
int x,y,z,area;
};
_data data[200];
bool com(_data d1,_data d2)
{
return d1.area<d2.area;
}
int n;
int dp[200];
int main()
{
int count=1;
while(scanf("%d",&n)!=EOF&&n)
{
int index=0;
for(int i=0;i<n;i++)
{
int x,y,z;
scanf("%d %d %d",&x,&y,&z);
data[index].x=x;data[index].y=y;data[index].z=z;data[index++].area=x*y;
data[index].x=y;data[index].y=x;data[index].z=z;data[index++].area=x*y;
data[index].x=y;data[index].y=z;data[index].z=x;data[index++].area=z*y;
data[index].x=z;data[index].y=y;data[index].z=x;data[index++].area=z*y;
data[index].x=x;data[index].y=z;data[index].z=y;data[index++].area=x*z;
data[index].x=z;data[index].y=x;data[index].z=y;data[index++].area=x*z;
}
sort(data,data+index,com);
memset(dp,0,sizeof(dp));
dp[0]=data[0].z;
for(int i=1;i<index;i++)
{
int temp=0;
for(int j=0;j<i;j++)
if(data[i].x>data[j].x && data[i].y>data[j].y&&temp<dp[j])
temp=dp[j];
dp[i]=temp+data[i].z;
}
int ans=dp[0];
for(int i=1;i<index;i++)
ans=max(ans,dp[i]);
printf("Case %d: maximum height = %d\n",count++,ans);
}
return 0;
}
ProID 1074 Doing Homework
题目大意是,给定一些课程,包括截止期和扣分,问怎样编排课程是得扣分最少。
看了半天没有思路,只好google,用的是所谓的压缩DP,用一位表示一个状态,也算是个好题,至少学到了些基本的方法。
下面的大部分东西摘自GentleH的博客,代码里我增加了注释,应该还是比较容易理解的。
状态压缩DP
因为只有15个课程,用15位的二进制表示课程的完成状态,1表示完成,0表示未完。
假定有4门课程,1100表示的是第4门和第3门课是完成的,也就是说可以用12这个十进制数来表示完成了第4和第3门课这个状态。那么15门课程,如果表示全部都完成了,那么只需用32767来表示。
二进制操作:
(1)i & j:j为第j门课,则这个操作表示的是i这个状态是否包含了j,也就是得到i和j的相同之处。比如1011 & 0010 = 0010(!=0),说明i是包含j的。相同部分就是j。
(2)i ^ j: 对于i和j两个不同的状态,该操作的结果是得到i和j的不同之处。比如1011 ^ 0010 = 1001。说明了i这个状态除了j之外,还有第4门和第1门是完成的。这跟&操作恰好相反。
PS: i | j:将i和j两个状态合并,比如1011 & 0100 = 1111;
状态方程比较复杂,我解释一下。
dp[i]中有3个字段,pre,res,cost,分别表示上一个状态,需要扣的分,以及达到当前状态需要花费的时间。
这个下表i范围是0...1<<n,用二进制的每一位表示一个作业。
在当前状态i中,如果包含了课程cur,那么这个状态就是由这个课程转换而来的,规则是
状态i中的cost=状态 i^cur(出cur课程以外所有课程)的cost+完成课程cur所需要的时间。
如果到达状态i时所花费的总时间不超过课程cur的deadline,那么就不会增加扣分。否则需要记录这个扣分,咋那么总的扣分是状态i^cur的扣分+完成当前课程所扣的分(有就加,没有就不加)。现在需要确定是否状态转换,是否转换时候扣的分的多少而决定的。如果当前状态的扣分>由cur转换过来的扣分,那么就更新状态,即当前状态是由cur转换过来的。
代码如下:
[cpp] view plain copy print ?
- #include<iostream>
- #include<string>
- #include<cstring>
- #include<cstdio>
- using namespace std;
-
- struct Work
- {
- char name[105];
- int deadline;
- int cost;
- }work[20];
-
- struct DP
- {
- int pre;
- int res;
- int cost;
- }dp[40005];
- int N;
-
- void out(int state)
- {
- if(state>0)
- {
- out(dp[state].pre);
- int t=dp[state].pre;
- int ans=t ^ state;
-
- int idx=0;
-
- while(ans>0)
- {
- idx++;
- ans>>=1;
- }
- printf("%s\n",work[idx].name);
- }
- }
-
- int main()
- {
- int T;
- scanf("%d",&T);
- while(T--)
- {
- scanf("%d",&N);
- for(int i=1;i<=N;i++)
- scanf("%s %d %d",&work[i].name,&work[i].deadline,&work[i].cost);
- int Max=(1<<N )-1;
- dp[0].cost=0;
- dp[0].pre=-1;
- dp[0].res=0;
-
- for(int i=1;i<=Max;i++)
- {
- dp[i].cost=0;
- dp[i].pre=-1;
- dp[i].res=0xfffffff;
- for(int j=0;j<N;j++)
- {
- int cur=1<<j;
- if(i & cur)
- {
- dp[i].cost=dp[i^cur].cost+work[j+1].cost;
- int reduce=dp[i].cost-work[j+1].deadline;
- if(reduce<0)
- reduce=0;
- reduce += dp[i^cur].res;
-
- if(dp[i].res>=reduce)
- {
- dp[i].res=reduce;
- dp[i].pre=i^cur;
- }
- }
- }
- }
- printf("%d\n",dp[Max].res);
- out(Max);
- }
- return 0;
- }
#include<iostream>
#include<string>
#include<cstring>
#include<cstdio>
using namespace std;
struct Work
{
char name[105];
int deadline;
int cost;
}work[20];
struct DP
{
int pre;
int res;
int cost;
}dp[40005];
int N;
void out(int state)
{
if(state>0)
{
out(dp[state].pre);
int t=dp[state].pre;
int ans=t ^ state;//找出当前状态和上一状态的不同之处
//也就是当前的课程
int idx=0;
//找出该十进制数在二进制表示下,1出现在什么位置,因为1代表完成了哪个课程。
while(ans>0)
{
idx++;
ans>>=1;
}
printf("%s\n",work[idx].name);
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
scanf("%d",&N);
for(int i=1;i<=N;i++)
scanf("%s %d %d",&work[i].name,&work[i].deadline,&work[i].cost);
int Max=(1<<N )-1;
dp[0].cost=0;
dp[0].pre=-1;
dp[0].res=0;
for(int i=1;i<=Max;i++)
{
dp[i].cost=0;
dp[i].pre=-1;
dp[i].res=0xfffffff;
for(int j=0;j<N;j++)
{
int cur=1<<j;
if(i & cur) //包含了该课程
{//状态,pre是上一个状态,res是需要扣的分,cost是达到当前状态需要花费的时间
dp[i].cost=dp[i^cur].cost+work[j+1].cost;
int reduce=dp[i].cost-work[j+1].deadline;
if(reduce<0)
reduce=0;
reduce += dp[i^cur].res;
//因为题目中的课程输入已经保证是字典序,所以==的时候一样更新
if(dp[i].res>=reduce)
{
dp[i].res=reduce;
dp[i].pre=i^cur;
}
}
}
}
printf("%d\n",dp[Max].res);
out(Max);
}
return 0;
}
ProID 1078 FatMouse and Chese
题目大意是,给你一个二维数组,从0,0开始,找到一条最长的路,从(x1,y1) ->(x2,y2)
要满足 x1==x2&& |y1-y2|<=k 或者 y1==y2&&|x1-x2|<=k。
PS:一定注意啊,是上下都可以走,左右可也走,最开始我以为是只能向下或者向右走,郁闷了半天。
对于路径问题,一般都是深度搜索,所以我们也用DFS方法。
解释一下状态转移方程,dp[i][j]表示以第i行第j列个网格为起点所能得到的最大值,注意了因为是深度搜索,是不断向下走的,所以状态的转移是以下边的状态为基础的,而不是以前边的状态为基础,因此就不是这个意思:dp[i][j]表示从0,0到i,j的最大值,这是绝对错误的。
状态转移方程为:dp[i][j] = max{dp[i + d1*c][j + d2*c], 1=< c <= k} + map[i][j]。
代码如下:
[cpp] view plain copy print ?
- #include <iostream>
- #include <cstdio>
- #include <cstring>
- #include <algorithm>
- using namespace std;
-
- int data[101][101];
- int dp[101][101];
-
- int dir[4][2]={{1,0},{-1,0},{0,1},{0,-1}};
-
- int n,k;
-
- int Dfs(int x,int y)
- {
- if(dp[x][y]>0)
- return dp[x][y];
- int Max=0;
- for(int i=0;i<4;i++)
- for(int j=1;j<=k;j++)
- {
- int tx=x+dir[i][0]*j;
- int ty=y+dir[i][1]*j;
-
- if(tx>=0 && tx<n && ty>=0 && ty<n && data[x][y]<data[tx][ty])
- {
- int temp=Dfs(tx,ty);
- Max=max(Max,temp);
- }
- }
-
- dp[x][y]=Max+data[x][y];
- return dp[x][y];
- }
-
- int main()
- {
- while(scanf("%d %d",&n,&k)!=EOF)
- {
- if(n==-1 && k==-1)
- break;
- for(int i=0;i<n;i++)
- for(int j=0;j<n;j++)
- scanf("%d",&data[i][j]);
- memset(dp,0,sizeof(dp));
- Dfs(0,0);
- printf("%d\n",dp[0][0]);
- }
- return 0;
- }
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int data[101][101];
int dp[101][101];//dp[i][j]表示从位置(i,j)为起点时能吃到最多的
int dir[4][2]={{1,0},{-1,0},{0,1},{0,-1}};
int n,k;
int Dfs(int x,int y)
{
if(dp[x][y]>0)//如果已经有值了,可以直接返回
return dp[x][y];
int Max=0;
for(int i=0;i<4;i++)//搜四个方向
for(int j=1;j<=k;j++)//一次可以跳1至k步
{
int tx=x+dir[i][0]*j;
int ty=y+dir[i][1]*j;
//必须跳的位置的奶酪比当前所站的位置多
if(tx>=0 && tx<n && ty>=0 && ty<n && data[x][y]<data[tx][ty])
{
int temp=Dfs(tx,ty);
Max=max(Max,temp);
}
}
dp[x][y]=Max+data[x][y];
return dp[x][y];
}
int main()
{
while(scanf("%d %d",&n,&k)!=EOF)
{
if(n==-1 && k==-1)
break;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
scanf("%d",&data[i][j]);
memset(dp,0,sizeof(dp));
Dfs(0,0);
printf("%d\n",dp[0][0]);
}
return 0;
}
ProID 1080 Human Gene Function
这个题目跟那个最长公共子序列问题很像,搞生物信息学的同学对这个应该很熟悉了,这个题目应该算是最基础的,跟其他算法比起来可真是小巫见大巫了。
好了不说了。题目意思是,给两个字符串,包含ATGC,然后可以插入一个空格,根据题目中所给的二维表中的评分来确定最大分值(就是确定插入空格的位置)
PS: 注意了,两个字符串都可以插入空格,但是最终的字符串都必须长度相等。
状态转移方程为:
d[i][j]=max(d[i-
1][j-1]+match[ s1[i] ][ s2[j] ] , d[i-1][j]+match[ s1[i] ][ '-' ] , d[i][j-1]+match[ '-' ][ s2[j] ] );
这里的边界条件需要注意,跟那个LCS不太一样:
d[0][0]=0; d[i][0]=d[i-1][0] + match[ s1[i] ][ '-' ] ; d[0][i]=d[0][i-1] + match[ '-' ][ s2[i] ];
如果有d[i][j] = d[i-1][j]+match[ s1[i] ][ '-' ] 表示 s1[i-1] 与 s2[j] 匹配 ,
于是 s1[i] 就只能和 ‘-’(空格)匹配了。
代码如下:
[cpp] view plain copy print ?
- #include <iostream>
- #include <cstdio>
- #include <cstring>
- #include <algorithm>
- #include <map>
- #include <string>
- using namespace std;
-
-
- map<string,int>data;
-
- int dp[110][110],match[110][110];
- char s1[110],s2[110];
-
- int T;
-
- void init()
- {
- match['A']['A']=match['C']['C']=match['G']['G']=match['T']['T']=5;
- match['A']['C']=match['C']['A']=match['A']['T']=match['T']['A']=-1;
- match[' ']['T']=match['T'][' ']=-1;
- match['A']['G']=match['G']['A']=match['C']['T']=match['T']['C']=-2;
- match['G']['T']=match['T']['G']=match['G'][' ']=match[' ']['G']=-2;
- match['A'][' ']=match[' ']['A']=match['C']['G']=match['G']['C']=-3;
- match['C'][' ']=match[' ']['C']=-4;
- }
-
- int main()
- {
-
- init();
- while(scanf("%d",&T)!=EOF)
- {
- int n1,n2;
- while(T--)
- {
- scanf("%d %s",&n1,s1+1);
- scanf("%d %s",&n2,s2+1);
- memset(dp,0,sizeof(dp));
-
- for(int i=1;i<=n1;i++)
- {
- dp[i][0]=dp[i-1][0]+match[s1[i]][' '];
- }
-
- for(int i=1;i<=n2;i++)
- {
- dp[0][i]=dp[0][i-1]+match[' '][s2[i]];
- }
- for(int i=1;i<=n1;i++)
- for(int j=1;j<=n2;j++)
- {
- int tmp1=dp[i-1][j-1]+match[s1[i]][s2[j]];
- int tmp2=dp[i-1][j]+match[s1[i]][' '];
- int tmp3=dp[i][j-1]+match[' '][s2[j]];
-
- int res=max(tmp1,max(tmp2,tmp3));
- dp[i][j]=res;
- }
- printf("%d\n",dp[n1][n2]);
- }
- }
- return 0 ; }
博客转自:http://blog.csdn.net/xueerfei008/article/details/9143783