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]中存储内容有两种:
- 当前状态下最后完成的任务,此时便于递归输出方案tk[pre[s]].name,且题目的输入就是按字典序从小到大,我们从前向后枚举,字典序小的对应状态s的低位,最终结果的输出也为字典序,递归为上一个状态的公式为:s=s^(1<<(pre[s]-1))或s-(1<<(pre[s]-1))
- 当前状态的前一个状态,直接更新**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;
}