2018"百度之星"程序设计大赛
题目1
调查问卷
度度熊为了完成毕业论文,需要收集一些数据来支撑他的论据,于是设计了一份包含 m 个问题的调查问卷,每个问题只有 ‘A’ 和 ‘B’ 两种选项。 将问卷散发出去之后,度度熊收到了 n 份互不相同的问卷,在整理结果的时候,他发现可以只保留其中的一部分问题,使得这 n 份问卷仍然是互不相同的。这里认为两张问卷是不同的,当且仅当存在至少一个被保留的问题在这两份问卷中的回答不同。 现在度度熊想知道,存在多少个问题集合,使得这 n 份问卷在只保留这个集合的问题之后至少有 k 对问卷是不同的。
input
第一行包含一个整数 T,表示有 T 组测试数据。 接下来依次描述 T 组测试数据。对于每组测试数据: 第一行包含三个整数 n,m 和 k,含义同题目描述。 接下来 n 行,每行包含一个长度为 m 的只包含 ‘A’ 和 ‘B’ 的字符串,表示这份问卷对每个问题的回答。 保证 1≤T≤100,1≤n≤103,1≤m≤10,1≤k≤106,给定的 n 份问卷互不相同。
output
对于每组测试数据,输出一行信息 “Case #x: y”(不含引号),其中 x 表示这是第 x 组测试数据,y 表示满足条件的问题集合的个数,行末不要有多余空格。
Sample Input
2
2 2 1
AA
BB
2 2 2
AA
BB
Sample Output
Case #1: 3
Case #2: 0
分析
注意到m很小,可以暴力枚举所有的子集然后判断不同的对数是否大于等于k。注意可能有多个人的问卷是一样的。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define pb push_back
#define inf 0x3f3f3f3f
int vis[1111];
char e[1111][20];
int main(){
int t,n,q,m,i,j,cas=0,k;
scanf("%d",&t);
while(t--){
scanf("%d%d%d",&n,&m,&k);
for(i=0;i<n;++i) scanf("%s",e[i]);
int ans=0,all=((1<<m)-1);
for(int i=0;i<=all;++i){
int x=0,y=0;
memset(vis,0,sizeof(vis));
for(int j=0;j<n;++j){
x=0;
for(int k=0;k<m;++k){
if(i&(1<<k)){
x<<=1;
if(e[j][k]=='B') x|=1;
}
}
vis[x]++;
}
LL ss=0,tmp=0;
for(int j=0;ss<n;++j){
if(!vis[j]) continue;
tmp+=1LL*vis[j]*(n-ss-vis[j]);
ss+=vis[j];
}
if(tmp>=k)ans++;
}
printf("Case #%d: %d\n",++cas,ans);
}
return 0;
}
题目2
子串查询
度度熊的字符串课堂开始了!要以像度度熊一样的天才为目标,努力奋斗哦! 为了检验你是否具备不听课的资质,度度熊准备了一个只包含大写英文字母的字符串 A[1,n]=a1a2⋯an,接下来他会向你提出 q 个问题 (l,r),你需要回答字符串 A[l,r]=alal+1⋯ar 内有多少个非空子串是 A[l,r] 的所有非空子串中字典序最小的。这里的非空子串是字符串中由至少一个位置连续的字符组成的子序列,两个子串是不同的当且仅当这两个子串内容不完全相同或者出现在不同的位置。 记 |S| 为字符串 S 的长度,对于两个字符串 S 和 T ,定义 S 的字典序比 T 小,当且仅当存在非负整数 k(≤min(|S|,|T|)) 使得 S 的前 k 个字符与 T 的前 k 个字符对应相同,并且要么满足 |S|=k 且 |T|>k,要么满足 k<min(|S|,|T|) 且 S 的第 k+1 个字符比 T 的第 k+1 个字符小。例如 “AA” 的字典序比 “AAA” 小,“AB” 的字典序比 “BA” 小。
Input
第一行包含一个整数 T,表示有 T 组测试数据。 接下来依次描述 T 组测试数据。对于每组测试数据: 第一行包含两个整数 n 和 q,表示字符串的长度以及询问的次数。 第二行包含一个长为 n 的只包含大写英文字母的字符串 A[1,n]。 接下来 q 行,每行包含两个整数 li,ri,表示第 i 次询问的参数。 保证 1≤T≤10,1≤n,q≤105,1≤li≤ri≤n。
Output
对于每组测试数据,先输出一行信息 “Case #x:”(不含引号),其中 x 表示这是第 x 组测试数据,接下来 q 行,每行包含一个整数,表示字符串 A[l,r] 中字典序最小的子串个数,行末不要有多余空格。
Sample Input
1
2 3
AB
1 1
1 2
2 2
Sample Output
Case #1:
1
1
1
分析
字典序最小的子串,显然长度是1,做个前缀和统计一下不同字母出现的次数就好了。然后找区间内最小的且出现次数不为零的字母就是答案。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define pb push_back
#define inf 0x3f3f3f3f
char s[100010];
LL f[100010][26];
int main(){
int t,n,q,m,i,j,k,cas=0,l,r;
scanf("%d",&t);
while(t--){
scanf("%d%d%s",&n,&q,s+1);
for(i=1;i<=n;++i){
for(j=0;j<26;++j) f[i][j]=f[i-1][j];
f[i][s[i]-'A']++;
}
printf("Case #%d:\n",++cas);
while(q--){
scanf("%d%d",&l,&r);
LL ans=0;
for(i=0;i<26;++i){
if(f[r][i]-f[l-1][i]){
ans=f[r][i]-f[l-1][i];
break;
}
}
printf("%I64d\n",ans);
}
}
return 0;
}
题目3
序列计数
度度熊了解到,1,2,…,n 的排列一共有 n!=n×(n−1)×⋯×1 个。现在度度熊从所有排列中等概率随机选出一个排列 p1,p2,…,pn,你需要对 k=1,2,3,…,n 分别求出长度为 k 的上升子序列个数,也就是计算满足 1≤a1 < a2 < … < ak ≤n 且 pa1 <pa2< … < pak 的 k 元组 (a1,a2,…,ak) 的个数。 由于结果可能很大,同时也是为了 ruin the legend, 你只需要输出结果对 1000000007(=109+7) 取模后的值。
Input
第一行包含一个整数 T,表示有 T 组测试数据。 接下来依次描述 T 组测试数据。对于每组测试数据: 第一行包含一个整数 n,表示排列的长度。 第二行包含 n 个整数 p1,p2, …, pn,表示排列的 n 个数。 保证 1≤T≤100,1≤n≤104,T 组测试数据的 n 之和 ≤105,p1,p2,…,pn 是 1,2,…,n 的一个排列。 除了样例,你可以认为给定的排列是从所有 1,2,…,n 的排列中等概率随机选出的。
Output
对于每组测试数据,输出一行信息 “Case #x: c1 c2 … cn”(不含引号),其中 x 表示这是第 x 组测试数据,ci 表示长度为 i 的上升子序列个数对 1000000007(=109+7) 取模后的值,相邻的两个数中间用一个空格隔开,行末不要有多余空格。
Sample Input
2
4
1 2 3 4
4
1 3 2 4
Sample Output
Case #1: 4 6 4 1
Case #2: 4 5 2 0
分析
不难想到一个O(NNlog(N))的做法,但是这个数据一直担心会gg,写了之后也确实T了。后来经过大佬指点发现,由于数据是随机的,所以递推到某一个长度之后可能没有任何一种方案,那他之后更大的长度的方案个数也是0,就可以直接退出循环了。加一句话就过了。
f(i,j)表示以第i个数结尾的长度为j的上升序列的方案个数,转移的时候用BIT维护下,要保证当前BIT内的数值都是当前位之前的位置贡献的。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define pb push_back
#define inf 0x3f3f3f3f
LL mod=1e9+7;
int a[10010],N,M;
LL C[10010],ans[10010],f[2][10010];
inline int lowbit(int x){return x&-x;};
inline void add(int x,LL d){
while(x<=N){
C[x]+=d;
C[x]%=mod;
x+=lowbit(x);
}
}
inline LL sum(int x){
LL r=0;
while(x){
r+=C[x];
r%=mod;
x-=lowbit(x);
}
return r;
}
inline void scan_d(int &ret)
{
char c;
ret = 0;
while ((c = getchar()) < '0' || c > '9');
while (c >= '0' && c <= '9')
{
ret = ret * 10 + (c - '0'), c = getchar();
}
}
int main(){
int t,n,m,i,j,k,cas=0;
char ch;
scanf("%d",&t);
while(t--){
scanf("%d",&n);
N=n;
for(i=1;i<=n;++i) scan_d(a[i])/*scanf("%d",a+i)*/,f[0][i]=1;
ans[1]=n;
int cur=1;
for(int len=2;len<=n;++len){
ans[len]=0;
if(ans[len-1]==0) continue;
for(i=1;i<=n;++i){
LL tmp=sum(a[i]-1);
ans[len]+=tmp;
add(a[i],f[cur^1][i]);
f[cur][i]=tmp;
}
//memset(C,0,sizeof(C));
for(i=1;i<=n;++i)C[i]=0;
cur^=1;
}
printf("Case #%d:",++cas);
for(i=1;i<=n;++i) printf(" %I64d",ans[i]%mod);
cout<<endl;
}
return 0;
}
题目4
三原色图
度度熊有一张 n 个点 m 条边的无向图,所有点按照 1,2,⋯,n 标号,每条边有一个正整数权值以及一种色光三原色红、绿、蓝之一的颜色。 现在度度熊想选出恰好 k 条边,满足只用这 k 条边之中的红色边和绿色边就能使 n 个点之间两两连通,或者只用这 k 条边之中的蓝色边和绿色边就能使 n 个点之间两两连通,这里两个点连通是指从一个点出发沿着边可以走到另一个点。 对于每个 k=1,2,⋯,m,你都需要帮度度熊计算选出恰好 k 条满足条件的边的权值之和的最小值。
Input
第一行包含一个正整数 T,表示有 T 组测试数据。 接下来依次描述 T 组测试数据。对于每组测试数据: 第一行包含两个整数 n 和 m,表示图的点数和边数。 接下来 m 行,每行包含三个整数 a,b,w 和一个字符 c,表示有一条连接点 a 与点 b 的权值为 w、颜色为 c 的无向边。 保证 1≤T≤100,1≤n,m≤100,1≤a,b≤n,1≤w≤1000,c∈R,G,B,这里 R,G,B 分别表示红色、绿色和蓝色。
Output
对于每组测试数据,先输出一行信息 “Case #x:”(不含引号),其中 x 表示这是第 x 组测试数据,接下来 m 行,每行包含一个整数,第 i 行的整数表示选出恰好 i 条满足条件的边的权值之和的最小值,如果不存在合法方案,输出 −1,行末不要有多余空格。
Sample Input
1
5 8
1 5 1 R
2 1 2 R
5 4 5 R
4 5 3 G
1 3 3 G
4 3 5 G
5 4 1 B
1 2 2 B
Sample Output
Case #1:
-1
-1
-1
9
10
12
17
22
分析
跑两次kruscal就好了,一次只用条件一的边,一次用条件二的边。每次跑完之后在依次加入剩下的边更新ans,如果无法联通答案就都是-1了。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define pb push_back
#define inf 0x3f3f3f3f
int f[111];
int getf(int u){return f[u]==u?u:f[u]=getf(f[u]);}
struct Edge{
int u,v,w,col,sel;
bool operator<(const Edge& C)const{
return w<C.w;
}
}e[110];
int ans[110],N,M;
void mst(int no){
int cnt=0,cnw=0;
for(int i=1;i<=N;++i)f[i]=i;
for(int i=1;i<=M;++i)e[i].sel=0;
for(int i=1;cnt<N-1&&i<=M;++i){
if(e[i].col==no) continue;
int fu=getf(e[i].u),fv=getf(e[i].v);
if(fu!=fv){
e[i].sel=1;
cnt++;
cnw+=e[i].w;
f[fv]=fu;
}
}
if(cnt!=N-1) return;
if(ans[cnt]==-1) ans[cnt]=cnw;
else ans[cnt]=min(ans[cnt],cnw);
for(int i=1;i<=M;++i){
if(e[i].sel) continue;
cnt++;
cnw+=e[i].w;
if(ans[cnt]==-1) ans[cnt]=cnw;
else ans[cnt]=min(ans[cnt],cnw);
}
}
int main(){
int t,n,m,i,j,k,cas=0;
char ch;
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
N=n,M=m;
for(i=1;i<=m;++i){
scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
//getchar();scanf("%c",&ch);
cin>>ch;
if(ch=='R') e[i].col=0;
else if(ch=='G') e[i].col=1;
else if(ch=='B') e[i].col=2;
}
sort(e+1,e+1+m);
memset(ans,-1,sizeof(ans));
mst(0);
mst(2);
printf("Case #%d:\n",++cas);
for(i=1;i<=m;++i) printf("%d\n",ans[i]);
}
return 0;
}
收获
此次和同学组队参加算法比赛收获颇丰,以下我逐一列出自己的所得:
1、保证每次运行结果都是符合题目要求的
在复杂的场景下,你的算法可能是非常优秀的,但是可能在一个很微小的点上,是不符合题目要求的。题目要求是最大的前提,如果违反题目要求,再好的算法都是不能被采纳的。所以务必检查每次的运行结果是否符合题目要求。
2、保证编码正确
有时候,一个想法是正确的,但是往往可能出现编码的错误,使得结果变坏。所以,必须保证你的代码和你的想法是一致的。如果出现你的想法使得你的运行结果变差的时候,不要先急着推翻你的想法,而是应该先看一下是不是你的代码有什么错误,没有真正反映出你的实际想法。
3、逻辑和阈值
在复杂的算法题目中,有时候我们喜欢用阈值来运行出不同结果,然后选择最优的结果。但是,如果可能的话,应该以统计等方式决定阈值。如果可能的话,应该有一个明确的逻辑,而不使用阈值。阈值没有泛用性,并且有时候由于不能测试每一个阈值的效果,容易出现局部最优解,而不是全局最优解
4、版本管理
不是每一个想法都会给结果带来正面的效果,有些改动可能不但没有效果,而且使得结果变坏,我们需要一个版本管理的概念,将不好的结果及时回滚到原来的状态。有时候,两个想法可能被同事加入到代码中,并且两个想法独立使用和一起使用效果也不同,这个时候更加需要版本管理工具了。建议使用Git做版本管理工具,可以自由的添加或者删减不同的想法。
5、中间指标 和 数据分析
在最终目标之外,还需要订立一些其他指标来判断算法的好坏。
整个最终结果往往是由多种因素决定的,如果将这些因素的量化结果也打印出来,对于这些指标进行分析,往往可以找到优化点。
有一些题目,在写算法之前,可以将题目中给定的数据集进行一些分析,总结出数据集的特征,然后针对这些特征选择算法,也是一个很重要的步骤。
6、从答案中找问题
很多算法的最终结果可能是一些调度计划和分配方案。仔细观察这些结果,找到一些明显可以优化的点,这也是提高算法结果的途径。
从算法执行结果这个答案中寻找你的算法中存在的问题,是一条很有用的途径。