洛谷 [USACO17OPEN]Bovine Genomics G奶牛基因组(金) ———— 1道骗人的二分+trie树(其实是差分算法)
题目 :Bovine Genomics G奶牛基因组
传送门: 洛谷P3667
题目描述
Farmer John owns NN cows with spots and NN cows without spots. Having just completed a course in bovine genetics, he is convinced that the spots on his cows are caused by mutations in the bovine genome.
At great expense, Farmer John sequences the genomes of his cows. Each genome is a string of length MM built from the four characters A, C, G, and T. When he lines up the genomes of his cows, he gets a table like the following, shown here
for N=3N=3 and M=8M=8 :
Positions: 1 2 3 4 5 6 7 8
Spotty Cow 1: A A T C C C A T
Spotty Cow 2: A C T T G C A A
Spotty Cow 3: G G T C G C A A
Plain Cow 1: A C T C C C A G
Plain Cow 2: A C T C G C A T
Plain Cow 3: A C T T C C A T
Looking carefully at this table, he surmises that the sequence from position 2 through position 5 is sufficient to explain spottiness. That is, by looking at the characters in just these these positions (that is, positions 2 \ldots 52…5 ), Farmer John can predict which of his cows are spotty and which are not. For example, if he sees the characters GTCG in these locations, he knows the cow must be spotty.
Please help FJ find the length of the shortest sequence of positions that can explain spottiness.
FJ有一些有斑点和一些没有斑点的牛,他想搞清楚到底什么基因控制这个牛有没有斑点。
于是他找了n有斑点的牛和n头没有斑点的牛
这些牛的基因长度为m(基因中之包含ATCG四个字母)
求这个序列中的一个子串,可以确定是否有斑点。
子串需要符合要求:有斑点的牛这部分的子串,不能和无斑点的牛的这部分子串相同
求最短子串长度(就是说条件为任意相同范围的区间中,斑点牛群和无斑点牛群中任意一对牛的基因不相等。在此条件下要求最短的区间长度)
输入输出格式
输入格式:
The first line of input contains NN ( 1 \leq N \leq 5001≤N≤500 ) and MM ( 3 \leq M \leq 5003≤M≤500 ). The next NN lines each contain a string of MM characters; these describe the genomes of the spotty cows.
The final NN lines describe the genomes of the plain cows. No spotty cow has the same exact genome as a plain cow.
输出格式:
Please print the length of the shortest sequence of positions that is sufficient to explain spottiness. A sequence of positions explains spottiness if the spottiness trait can be predicted with perfect accuracy among Farmer John’s population of cows by looking at just those locations in the genome.
输入输出样例
输入样例#1:
3 8
AATCCCAT
ACTTGCAA
GGTCGCAA
ACTCCCAG
ACTCGCAT
ACTTCCAT
输出样例#1:
4
最近都在做trie树的题目,然而题目要么就是太bug要么就是没什么好讲的。but,but!我居然刷到了一道披着trie树皮最后要用差分A的无耻骗人trie树题!说好的trie树呢…TAT
一度分析
那么先来讲讲我是怎么“错”的吧。
首先看到这道题我果断用trie思考。首先二分答案,然后暴力枚举构造trie(整个就是错误的算法,与题意都不符然而就是A了一个点,因为题目中说的是相同范围的区间,我是全部区间弄一块儿找了,答案明显会偏大)。
然后代码如下我也不多废话,只能说,看看就好,看看就好。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int M=510;
inline int read(){
int x=0,f=1; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
return x*f;
}
int n,m,cnt;
int a[M*2][M];
int t[M*M*10][5]; //一棵trie树
inline bool check(int mid){
//这是模板吧?trie树。
for(int i=1;i<=n;++i)//枚举每头有斑点的牛
for(int j=1;j+mid-1<=m;++j){//枚举左边界(同时右边界也定了)
for(int now=0,k=j;k<=j+mid-1;++k){ //然后是trie树
int tmp=a[i][k];
if(!t[now][tmp])
t[now][tmp]=++cnt;
now=t[now][tmp];
}
}
for(int i=n+1;i<=2*n;++i) //枚举无斑点的牛
for(int j=1;j+mid-1<=m;++j){//枚举左边界(同上)
int now=0,flag=1;
for(int k=j;k<=j+mid-1 && flag;++k){
int tmp=a[i][k];
if(!t[now][tmp]) flag=0;//如果说有不相同的了就flag标成false
now=t[now][tmp];
}
if(flag) return false;//如果是能搜到底的就直接返回false
}
return true; //没有一头无斑点牛基因能在trie树中完全匹配那就满足条件,返回true
}
int main(){
n=read(); m=read();
for(int i=1;i<=n*2;++i)
for(int j=1;j<=m;++j){
char c=getchar(); while(!isupper(c)) c=getchar();
switch(c){
case 'A': a[i][j]=1; break;
case 'T': a[i][j]=2; break;
case 'C': a[i][j]=3; break;
case 'G': a[i][j]=4; break;
}
}
int l=1 , r=n;
while(l<=r){ //标准二分,搞得主函数很简洁
memset(t , 0 , sizeof(t));
int mid=l+r>>1; cnt=0;
if(check(mid)) r=mid-1; //满足条件则答案可以更小
else l=mid+1;//否则答案要更大
}
printf("%d\n",l);
return 0;
}
对于该程序的正确性,不用我说,洛谷显示是这样的:
看到了这样的结果后,本人表示…emmm…不服!不A此题誓不罢休!
然后看了看某dalao的解题报告之后,发现这道题。。。原来可以不用trie树做…(心中一万匹草泥马在奔腾)。
那么用什么算法呢?这里先留个悬念。我们先来看看一道题:
题目:???(记不清了_(:з」∠)_
)
题意:这个记得清(不然也没法讲)。
题意就是给你一个长度为n的数字序列,让你选出一段最短的区间,使该区间内所有数字的和为给定的一个整数k。
然后输出该最短区间的长度。
输入格式:
第一行是一个n和k。
第二行是n个数。
第三行。。。。没了
输出格式
一行一个数,表示最短的区间长度
数据范围
还是不知道着就n<=1e5,然后k<=1e5, too吧!
另外当然也保证数字序列中的每一个数都是正整数咯。
分析
相信大多数人瞬间想到的就是枚举左右区间,前缀和再减一减。
但是一看到数据范围的时候就会懵逼了:1e5平方一下瞬间就T了。可见标算不会是暴力的。
那么如果用差分的方法呢,时间复杂度瞬间就会达到一个可怕的地步:O(n)。然后稳过。
那么怎么差分呢?首先我们考虑:在我们暴力枚举左右边界时,其实有很多趟循环是无意义的。
何来浪费一说?比如,有一个序列是(2,5,3,2,2,1,3),k为7,
那么假如我们已经做过了左区间i=1,右区间j=3的情况,发现sum已经大于k了,
那么此时我们还需要考虑i=1 ,j=4、5、6的情况么?
答案很显然(抱歉用了一个我很讨厌的词_(:з」∠)_)
于是我们要在这个方面对程序进行优化。(然而我还并没有讲如何优化)
如何优化? 首先我们可以先试着随意枚举一个区间,那么如果这个区间的sum是小于k的,
我们就需要将这个区间变大,然后才会使sum变大吧?那么向左还是向右扩大区间呢?
我们先不着急,这里还有一种情况,就是说sum已经大于k了,也就说明我们的区间取大了,
那么这两种情况很类似吧?于是我们不妨做个约定:sum 大 l 加,sum 小 r 加。
这里的l便代表了左边界,而r则代表了右边界。
那么我们还有一个疑问没有解决:初始的区间在哪里?中间?两边?还是随便取吗?
其实我们大概想想也就知道了,初始区间要在整个数字序列的最左边,即l和r重合。
emmmm...不对,应该是l>r,因为l=r的时候还是有一个数字被框在区间内的。
当然为了代码方便,我们也可以用l和r表示当前区间为l+1 ~ r,这样计算区间长的时候,
我们只需要将 r-l 就行了。
but,如果你是一名素质较高的OIer,那你在看完算法思路之后肯定会立刻抛出一个疑问,
那便是算法的正确与否,在这道题中就是说,有没有任意一个符合条件的区间会被遗漏?
那么这个问题这里就不解释了,童鞋们自个儿验证去吧! _(:з」∠)_
(然后学了莫队之后,现在想想,两种算法在某种程度上来讲真的还是有点像的呢...)
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int M=1e5+100;
inline int read(){
int x=0,f=1; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
return x*f;
}
int n,k,sum,res=M;
int a[M];
int main(){
n=read(); k=read(); //因为k没有爆int,sum也不会爆int
for(int i=1;i<=n;++i)
a[i]=read();
int lef=1,rig=1; //初始l、r指针都在1,当前区间为l+1 ~ r
while(rig<=n){
if(sum==k) res=min(res , rig-lef); //满足条件则维护最小区间长
if(sum<k){ //小了就让rig++,区间变大
sum+=a[rig];
++rig;
}
else{ //不然lef++,区间变小
sum-=a[lef];
++lef;
}
}
//特判无解(当然我也不知道题目中有没有说数据保证有解)
printf("%d\n",res==M ? -1 : res);
return 0;
}
时间复杂度:O(n),很稳能A(当然由于程序是现码的,题目也记不清所以并没有在 OJ 上测过,但总的来说算法思路还是正确的)。另外没题目这事儿…抱歉啦同志们,很遗憾我真的记不起来啦…
然后对于这道题同学们有了点赶脚了么?我不妨先说了,这道 t 就是可以用以上的差分算法+随机数hash 给A掉的…
二度分析
然后再讲讲我是怎么“做”的。
那么 随机数hash 我在这里也不打算说什么,不是很难懂的吧?就是一直莫名担心这随机数太随机用起来有点虚啊(万一…万一…取到的数字…太偶然呢?),然而落谷的数据是告诉我不会发生那种不好的事情的啦~
那么差分怎么弄?其实这玩意儿可以说是有个模板吧,就是说满足条件 l++,不满足条件 r++ ,上面那道题就是以sum > k 作为条件的。
这道题很显然啊,就是以没有字符串重合为条件的。
于是我也不啰嗦,直接上代码不浪费口舌了吧!
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=2333333;
set<ll> S;
int n,m,res;
int s[2][510][510];
int val[510],hash[2][510];
int main(){
res=0x7f7f7f7f;
srand(time(NULL));
scanf("%d%d",&n,&m);
for(int k=1;k>=0;--k)
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
char c=getchar(); while(!isupper(c)) c=getchar();
s[k][i][j]=c-'A'+1;
}
for(int i=1;i<=m;++i)
val[i]=rand()%mod; //无脑 rand hash 。
int lef=1,rig=1;
bool flag=false;
while(rig<=m){
S.clear();
if(flag) res=min(res , rig-lef); // 当前满足条件的子串为lef+1~rig
//假如此刻有斑点的奶牛群和无斑点的奶牛群中有奶牛hash值相等则将rig向后移
if(!flag){ //不满足条件
flag=true;
for(int i=1;i<=n;++i){
hash[1][i]=(hash[1][i]+val[rig]*s[1][i][rig])%mod;
S.insert(hash[1][i]);
}
for(int i=1;i<=n;++i){
hash[0][i]=(hash[0][i]+val[rig]*s[0][i][rig])%mod;
if(S.count(hash[0][i])) flag=false;
}
++rig;
}
//否则贪心一点将lef后移查看是否有更优解
else{ //满足条件
flag=true;
for(int i=1;i<=n;++i){
hash[1][i]=(hash[1][i]+mod-val[lef]*s[1][i][lef]%mod)%mod;
S.insert(hash[1][i]);
}
for(int i=1;i<=n;++i){
hash[0][i]=(hash[0][i]+mod-val[lef]*s[0][i][lef]%mod)%mod;
if(S.count(hash[0][i])) flag=false;
}
++lef;
}
}
printf("%d\n",res); //这里应该是保证有答案的吧。。。(从题目没有说无解的情况上来说)
return 0;
}
ok,Judge 课堂就这样愉快的结束了喜欢别忘点个赞哈~_(:з」∠)_