WEEK(12)作业——动态规划(区间DP、状压DP)

A - 必做题 - 1

题目描述

给出n个数,zjm想找出出现至少(n+1)/2次的数, 现在需要你帮忙找出这个数是多少?

输入

本题包含多组数据:
每组数据包含两行。
第一行一个数字N(1<=N<=999999) ,保证N为奇数。
第二行为N个用空格隔开的整数。
数据以EOF结束。

输出

对于每一组数据,你需要输出你找到的唯一的数。

Sample

Input

5
1 3 2 3 3
11
1 1 1 1 1 5 5 5 5 5 5
7
1 1 1 1 1 1 1

Output

3
5
1

解题思路

用数组a记录每个数出现的次数,a[num]为数num出现的次数,当大于(n+1)/2时,因为这个数是唯一的,则num直接赋给ans即可。

写的时候出现了一个问题就是不知道数组a开多大(不清楚输入数据的大小范围,也不知道有没有负数),尽量大的试了试1e5,幸好这里没卡我过了QAQ。

后来想想用map一对一映射就完事了,没有任何多余考虑和风险。

代码

直接用数组记录,不知道数据范围,有风险

#include<iostream>
#include<cstdio>
#include<cstring>

using namespace std;

const int maxn=1e5;
 
int n,num,ans;
int a[maxn];

int main() {
	while(cin>>n) {
		memset(a,0,sizeof(a));
		int t=(n+1)/2;
		while(n--) {
			cin>>num;
			a[num]++;
			if(a[num]>=t) ans=num;
 		}
 		cout<<ans<<endl;
	}
	return 0;
}

更新后,map记录出现次数

#include<iostream>
#include<cstdio>
#include<map>

using namespace std;

map<int,int> a;
int n,num,ans;

int main()
{
	while(cin>>n)
	{
		a.clear();
		int t=(n+1)/2;
		for(int i=1;i<=n;i++) {
			cin>>num;
			a[num]++;
			if(a[num]>=t) ans=b;
		}
		cout<<ans<<endl;
	}
	return 0;
}

B - 必做题 - 2

题目描述

zjm被困在一个三维的空间中,现在要寻找最短路径逃生!
空间由立方体单位构成。
zjm每次向上下前后左右移动一个单位需要一分钟,且zjm不能对角线移动。
空间的四周封闭。zjm的目标是走到空间的出口。
是否存在逃出生天的可能性?如果存在,则需要多少时间?

输入

输入第一行是一个数表示空间的数量。
每个空间的描述的第一行为L,R和C(皆不超过30)。
L表示空间的高度,R和C分别表示每层空间的行与列的大小。
随后L层,每层R行,每行C个字符。
每个字符表示空间的一个单元。’#‘表示不可通过单元,’.‘表示空白单元。
zjm的起始位置在’S’,出口为’E’。每层空间后都有一个空行。
L,R和C均为0时输入结束。

输出

每个空间对应一行输出。
如果可以逃生,则输出如下
Escaped in x minute(s).
x为最短脱离时间。

如果无法逃生,则输出如下
Trapped!

Sample

Input

3 4 5
S….
.###.
.##…
###.#

##.##
##…

#.###
####E

1 3 3
S##
#E#
###

0 0 0

Output

Escaped in 11 minute(s).
Trapped!

解题思路

找从’S’到’E’的最短路径,很自然想到宽搜,每一轮都前进一步,最先到达的即为最短路。

图从二维变到了三维,用三维数组存储,建立每个坐标的结构体,包括x,y,z的坐标以及从’S到该坐标位置的最短时间。

这个题还要重点注意的是输入换行(回车)时如何忽略,因为这里的图是用字符表示的。
string和char * s的输入都可以直接屏蔽换行符,这里采用输入到str再将字符加到图的数组表示里,也可以直接向cube[i][j]输入字符串

代码

#include<iostream>
#include<cstdio>
#include<queue>
#include<string>
#include<cstring>

using namespace std;

int dx[]={1,-1,0,0,0,0};
int dy[]={0,0,1,-1,0,0};
int dz[]={0,0,0,0,1,-1};
int l,r,c,ans;

char cube[31][31][31];
bool flag[31][31][31];
string str;

struct pos {
	int x,y,z;
	int t;
};

queue<pos> q;

int bfs(pos s) {
	memset(flag,0,sizeof(flag));
	while(!q.empty()) q.pop();
	
	q.push(s);
	flag[s.x][s.y][s.z]=1;
	
	while(!q.empty()) {
		pos a=q.front();
		q.pop();
		if(cube[a.x][a.y][a.z]=='E') return a.t;
		for(int i=0;i<6;++i) {
			int x=a.x+dx[i];
			int y=a.y+dy[i];
			int z=a.z+dz[i];
			if(x>=1&&x<=l&&y>=1&&y<=r&&z>=1&&z<=c&&cube[x][y][z]!='#'&&!flag[x][y][z]) {
				if(cube[x][y][z]=='E') return a.t+1;
				pos temp;
				temp.x=x;
				temp.y=y;
				temp.z=z;
				temp.t=a.t+1;
				q.push(temp);
				flag[x][y][z]=1;
			}
		}
	}
	return -1;
}

int main() {
	while(1) {
		scanf("%d %d %d\n",&l,&r,&c);
		if(l==0&&r==0&c==0) break;
		pos s;
		for(int i=1;i<=l;++i) {
			for(int j=1;j<=r;++j) {
				cin>>str;
				for(int k=0;k<c;++k) {
					cube[i][j][k+1]=str[k];
					if(str[k]=='S') {
						s.x=i,s.y=j,s.z=k+1;
						s.t=0;
					}	
				}
			}
		}
		ans=bfs(s);
		if(ans>=0) cout<<"Escaped in "<<ans<<" minute(s)."<<endl;
		else cout<<"Trapped!"<<endl;
	}
	return 0;
}

C - 必做题 - 3

题目描述

东东每个学期都会去寝室接受扫楼的任务,并清点每个寝室的人数。
每个寝室里面有ai个人(1<=i<=n)。从第i到第j个宿舍一共有sum(i,j)=a[i]+…+a[j]个人
这让宿管阿姨非常开心,并且让东东扫楼m次,每一次数第i到第j个宿舍sum(i,j)
问题是要找到sum(i1, j1) + … + sum(im,jm)的最大值。且ix <= iy <=jx和ix <= jy <=jx的情况是不被允许的。也就是说m段都不能相交。
注:1 ≤ i ≤ n ≤ 1e6 , -32768 ≤ ai ≤ 32767 人数可以为负数。。。。(1<=n<=1000000)

输入

输入m,输入n。后面跟着输入n个ai

输出

输出最大和

Sample

Input

1 3 1 2 3
2 6 -1 4 -2 3 -2 3

Output

6
8

Hint

数据量很大,需要scanf读入和dp处理。

解题思路

仔细读来,这是一道最大m子段和问题。确实很难,看了网上的讲解才慢慢搞懂。

在最大m子段和问题中,要求取m个互不相交子段,和为最大值。最大m子段和问题是最大子段和在子段个数上的推广,最大子段和问题是m=1的特殊情况。

在这个问题中,我们使用一个矩阵dp[ i ][ j ],他表示的意义是在前 j 项中被分为 i 段的最大 i 子段和,且必须包含着第j项,即以第j项结尾。

接着是一个递推过程。
求dp[ i ][ j ],有两种情况
1、dp[ i ][ j ] = dp[ i ] [ j-1 ] + a[ j ] ,即把第j项融合到第 j-1 项所在的子段中,子段数没变。因为dp[i]就代表已经将前面的j-1项分为了i段;
2、dp[ i ][ j ] = dp[ i-1 ] [ k] + a[ j ],(i-1<= k < =j-1 ),把第 j 项作为单独的一个子段,然后找前j-1项组成的i-1个子段最大的和,然后加上a[ j ] 。

根据上面的分析我们可以得出状态转移方程:
dp[i][j] = max(dp[i][j-1]+a[j],max(dp[i-1][k])+a[j]) (i-1 <= k < =j-1)

在这里插入图片描述
即若要确定蓝色块的值,则在黄色块中寻找一个最大值,与红色块比较,取大者加到自己上。

优化:
1、左下角那一半矩阵,是不用计算的,因为n个数字不可能分为n+1个子段,这里直接在每次加段时(i++时),令dp[i][i-1]为极小值即可,此时由转移方程,前i个数每个数各成一段,那么dp[i][i]只能为前i个数的和
2、每确定一个dp[ i ][ j ],只需用到本行和上一行,所以不用开多维数组也可以,省内存。这里开两个一维数组,maxn和dp,maxn记录上一行,dp记录当前行。
3、当对上一行黄块中的数字找最大值时,若用一个循环来找,容易超时。可以通过动态维护上一行中[i-1,j)的最大值,省去k的一重循环。优化方法是:maxn[j]直接表示前j个数划分成i-1段时的最大i-1子段和(第j个数未必在第i-1段里),在第j个数的dp计算完后,可以将max[j-1]更新成分成i段后的最大值,即对于本行的最大值****(因为该值在这一行不再被用到,只会在下一行再次被用到,对下一行来说存储的便是上一行分成i-1段的最大值),tmp此时存储的是前j-1个数分成i段最大i子段和,maxn[j-1]赋值后,更新为前j个数分成i段的最大子段和,此时maxn[j]不能被更新,存储的仍是上一行的最大值,因为进入下一次j+1的循环时,计算dp[j+1]时还会被用到

其他优化:
如下图所示,沿着第m行的最后一个元素,往左上方向画一条线,线右上方的元素是没必要计算的。那么dp[ i ][ j ] ,j++的时候,j的上限为 i + n - m 即可。代码中未涉及此优化。
在这里插入图片描述
参考链接:
动态规划:最大M子段和,m
最大m子段和总结与例题

代码

#include<cstdio>
#include<iostream>
#include<cstring>

using namespace std;

const int MAXN=1e6+10;
const int INF=0x3f3f3f3f;

typedef long long ll;

int m,n;
int a[MAXN];
ll dp[MAXN],maxn[MAXN];

int main() {
	while(scanf("%d%d",&m,&n)!=EOF) {
		memset(dp,0,sizeof(dp));
		memset(maxn,0,sizeof(maxn));
		for(int i=1;i<=n;++i) 
			scanf("%d",a+i);
			
		ll tmp;	
		for(int i=1;i<=m;++i) {
			dp[i-1]=-INF;
			tmp=-INF;
			for(int j=i;j<=n;++j) {
				dp[j]=max(dp[j-1]+a[j],maxn[j-1]+a[j]);
				// maxn[j-1]已经被使用完毕,所以可以动态更新赋值了 
				maxn[j-1]=tmp;
				// t保存第i行、i~j列的最大dp值
                // 但是t不能马上赋值给maxn[j],因为下一次循环到j+1时,要用到原来的maxn[j]
				tmp=max(tmp,dp[j]);
			}
		}
		printf("%lld\n",tmp);
	}
	return 0;
}

D - 选做题 - 1

题目描述

We give the following inductive definition of a “regular brackets” sequence:

  • the empty sequence is a regular brackets sequence
  • if s is a regular brackets sequence, then (s) and [s] are regular brackets sequences
  • if a and b are regular brackets sequences, then ab is a regular brackets sequence.
  • no other sequence is a regular brackets sequence

For instance, all of the following character sequences are regular brackets sequences:

   (), [], (()), ()[], ()[()]

while the following character sequences are not:

   (, ], )(, ([)], ([(]

Given a brackets sequence of characters a1a2 … an, your goal is to find the length of the longest regular brackets sequence that is a subsequence of s. That is, you wish to find the largest m such that for indices i1, i2, …, im where 1 ≤ i1 < i2 < … < im ≤ n, ai1ai2 … aim is a regular brackets sequence.

Given the initial sequence ([([]])], the longest regular brackets subsequence is [([])].

输入

The input test file will contain multiple test cases. Each input test case consists of a single line containing only the characters (, ), [, and ]; each input test will have length between 1 and 100, inclusive. The end-of-file is marked by a line containing the word “end” and should not be processed.

输出

For each input case, the program should print the length of the longest possible regular brackets subsequence on a single line.

Sample

Input

((()))
()()()
([]])
)[)(
([][][)
end

Output

6
6
4
0
6

题目大意

给定一串只含()[]的字符串,从中选取最长的能够实现“正则”(括号合法匹配)的子序列,输出子序列长度。多组数据,end结束。

解题思路

子序列与子串不同之处在于子序列可以是不连续的。这道题是一道经典的区间dp的题。

首先定义f[i][j]表示整个括号序列的第 i 位至第 j 位中最长合法子序列的长度,初始化f[i][j]均为0。

由所给判定准则:

  • 如果S合法,那么(S)和[S]合法,即如果当前序列的左右两端括号匹配,则
    f[i][j] = f[i + 1][j - 1]+ 2
  • 如果A、B合法,则AB为合法
    f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j]),i<=k<j

由以上我们就得到了状态转移方程。
因为枚举时,较长的区间需要从较短的区间转移,因此首先枚举区间长度,step从1开始直到n-1。然后从头开始枚举起点i,终点j=i+step。
特别的,当[i,j]长度为2时,若两端括号匹配,不能用f[i][j] = f[i + 1][j - 1]+ 2更新,直接赋值为2。
注意多组数据初始化f,最终直接输出f[0][n-1]即可。

代码

#include<iostream>
#include<cstring>
#include<string>

using namespace std;

string s;
int f[110][110];

int main() {
	while(1) {
		cin>>s;
		if(s=="end") break;
		int n=s.size();
		memset(f,0,sizeof(f));
		for(int step=1;step<n;step++) {
			for(int i=0;i+step<n;++i) {
				int j=i+step;
				if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']')) {
					if(j==i+1) f[i][j]=2;
					else f[i][j]=f[i+1][j-1]+2;
				}
				for(int k=i;k<j;++k) 
					f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]);
			}
		}
		cout<<f[0][n-1]<<endl; 
	}
	return 0;
}

E - 选做题 - 2

题目描述

马上假期就要结束了,zjm还有 n 个作业,完成某个作业需要一定的时间,而且每个作业有一个截止时间,若超过截止时间,一天就要扣一分。
zjm想知道如何安排做作业,使得扣的分数最少。
Tips: 如果开始做某个作业,就必须把这个作业做完了,才能做下一个作业。

输入

有多组测试数据。第一行一个整数表示测试数据的组数
第一行一个整数 n(1<=n<=15)
接下来n行,每行一个字符串(长度不超过100) S 表示任务的名称和两个整数 D 和 C,分别表示任务的截止时间和完成任务需要的天数。

这 n 个任务是按照字符串的字典序从小到大给出。 

输出

每组测试数据,输出最少扣的分数,并输出完成作业的方案,如果有多个方案,输出字典序最小的一个。

Sample

Input

2
3
Computer 3 3
English 20 1
Math 3 2
3
Computer 3 3
English 6 3
Math 6 3

Output

2
Computer
Math
English
3
Computer
English
Math

Hint

在第二个样例中,按照 Computer->English->Math 和 Computer->Math->English 的顺序完成作业,所扣的分数都是 3,由于 English 的字典序比 Math 小,故输出前一种方案。

解题思路

本题数据范围较小(1<=n<=15),考虑状压dp,用s来表示当前完成的作业集合。
f[s]定义为完成s作业集合后被扣的最少分数。
n个任务,共2^n种状态。依次枚举每种状态。

转移方程:

  • sum=s作业集合对应的总花费时间
    可用 s&(1<<(i-1))判断状态s中是否已完成了第i项作业
  • tmp为若此时加入作业i,会被扣的分数(>=0)
    tmp=max(sum+tk[i].cc-tk[i].ddl,0),cc为作业i完成所需时间,ddl为作业i的ddl
  • 对每个状态s枚举所有任务,若该任务已在s中则跳过,否则更新新状态s|(1<<(i-1))(把i加到集合中)的f
    若新状态的当前f大于f[s]+tmp,即可更新为f[s]+tmp

从以上可知,一个状态s有多种到达途径,经过一次次枚举更新,最终所有的状态s的f[s]均为最小值。

注意:pre[s]中存储内容有两种:

  1. 当前状态下最后完成的任务,此时便于递归输出方案tk[pre[s]].name,且题目的输入就是按字典序从小到大,我们从前向后枚举,字典序小的对应状态s的低位,最终结果的输出也为字典序,递归为上一个状态的公式为:s=s^(1<<(pre[s]-1))s-(1<<(pre[s]-1))
  2. 当前状态的前一个状态,直接更新**s=pre[s]**即可,输出方案的公式为:
    tk[log2(s-pre[s])+1].name

从(1<<n)-1开始递归,直到s=0为终止条件,开始从前向后输出每个任务。

这题方法是状压DP,每个作业对应状态S中的一位,1表示完成了该作业,0表示未完成该作业,状态转移方程是f[S|(1<<X)]=f[S]+max(sum+c[x]-d[x],0)。
由于作业序号从1开始,用到X进行位运算时需要写成1<<(X-1)

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>

using namespace std;

struct task {
	string name;
	int ddl;
	int cc;
	
	//可不要,题目输入n 个任务是按照字符串的字典序从小到大给出
	bool operator < (const task &t) const {
		return name<t.name;
	}
}tk[16];

int T,n;
int f[1<<15],pre[1<<15];

void output(int s) {
	if(s==0) return;
	output(s^(1<<(pre[s]-1)));
	cout<<tk[pre[s]].name<<endl;
}

int main() {
	cin>>T;
	while(T--) {
		memset(f,0x7f,sizeof(f));
		memset(pre,0,sizeof(pre));
		f[0]=0;
		
		cin>>n;
		for(int i=1;i<=n;++i) 
			cin>>tk[i].name>>tk[i].ddl>>tk[i].cc;
			
		for(int s=0;s<(1<<n);++s) {
			int sum=0;//状态s下总时间 
			for(int i=1;i<=n;++i) 
				if(s&(1<<(i-1))) sum+=tk[i].cc;
				
			for(int i=1;i<=n;++i) {
				if(s&(1<<(i-1))) continue;
				
				int tmp=0;
				tmp=max(sum+tk[i].cc-tk[i].ddl,0);
				
				if(f[s|(1<<(i-1))]>f[s]+tmp) {
					f[s|(1<<(i-1))]=f[s]+tmp;
					pre[s|(1<<(i-1))]=i;
				}
			}
		}
		cout<<f[(1<<n)-1]<<endl;
		output((1<<n)-1);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值