蓝桥杯省赛 *3

一下题目均可在Acwing找到。

人物相关性分析  https://www.dotcpp.com/oj/problem2309.html

小明正在分析一本小说中的人物相关性。他想知道在小说中 Alice 和 Bob 有多少次同时出现。

更准确的说,小明定义 Alice 和 Bob“同时出现”的意思是:在小说文本 中 Alice 和 Bob 之间不超过 K 个字符。

例如以下文本:

This is a story about Alice and Bob. Alice wants to send a private message to Bob. 假设 K = 20,则 Alice 和 Bob 同时出现了 2 次,分别是”Alice and Bob”

和”Bob. Alice”。前者 Alice 和 Bob 之间有 5 个字符,后者有 2 个字符。 注意:

1. Alice 和 Bob 是大小写敏感的,alice 或 bob 等并不计算在内。
2. Alice 和 Bob 应为单独的单词,前后可以有标点符号和空格,但是不能

有字母。例如 Bobbi 並不算出现了 Bob。


思路:

  最容易想到的是枚举Alice和Bob在文本出现次数,构成两个整数数组,接着二重遍历求解,时间复杂度O(n^2)。但字符串输入长度范围是1e6,如果出现

如AliceBob连续出现的极端情况,时间就达到1e10量级,超时(上界在1e8左右)。

  如何由二重循环--->一重?一般有二分,哈希等方法。更进一步理解题意:观察某个首字母在 i 的Alice:与同时出现的Bob可能的范围在[ i - K - 3 , i + 5 + K ]。

而Alice的位置是单调递增的,所以同时出现的Bob其位置也是单调递增的,此时可以采用尺取法求解问题。尺取法通常是指对数组保存一对下标(起点,终点),

然后根据实际情况交替推进两个端点直到得出答案的方法,类似一段区间缓慢向前移动。

解题思路:

  首先分别求出Alice与Bob出现在文本中的位置,用下标 lp 保存第一次出现在可能范围的Bob,rp保存最后一次出现在范围的Bob,用 rp - lp + 1 即得到与

当前Alice同时出现的Bob的个数。

实现代码:

const int INF = 1e8;

int main()
{
        /*输入*/
    int K;
    string str;
    cin>>K;
    getchar();
    getline(cin,str); //输入带有空格的一行字符串 
    
    vector<int> alice,bob;
    /*保存出现的位置*/
    bob.push_back(-INF); /*统一计算*/
    int pos = 0;
    while( (pos = str.find("Bob",pos)) != string::npos ) //遍历Bob在str出现位置
    {
        bob.push_back(pos);
        pos++;    
    } 
    bob.push_back(INF);
    pos = 0;
    while( (pos = str.find("Alice",pos))!=string::npos )
    {
        alice.push_back(pos);
        pos++;
    }
    
        /*尺取法*/
    int lp = 0, rp = 0;
    int cize = alice.size();
    long long ans = 0;
    for( int i=0; i<cize; i++ )
    {
        while( bob[lp]<alice[i]-K-3 )     lp++;
        while( bob[rp+1]<=alice[i]+K+5 ) rp++; 
        if( rp-lp+1 > 0 )    ans += rp - lp + 1;
    }
    cout<<ans<<endl;
    return 0;
}    

这里的做法不够严格,不能避免Bobb情况。需要在判断Bob后面的那个单词不是字母。

/*
 *人物相关性分析 
 *    简单思路是找到Alice与Bob首字母出现个数 然后双重循环判断,当然超时
 *    蓝桥杯经常有枚举解决但会超时的题目 一个思路是减少枚举的变量
 *    其他变量用二分、哈希或者其他方法减少枚举次数
 *    而这一题我们观察到 对于Alice的位置 其Bob与其相关的区间为
 *    [Ai-3-K, Ai+5+k] Ai递增 所以区间也是不断向右移动的 可以用尺取法解决
 *        找到位置要注意单词的独立 
 */
#include<cctype>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;

typedef long long ll;

const int INF     = 1e8+10;
const int Max_N = 1e6+10;

int k;
char s[Max_N];
int length;
vector<int> alice, bob; //保存位置 

/*用很笨的方法判断 如果参数是指针会超时*/
bool checkA(int i)//Alice
{
    if( length - i < 5 )    return false;
    return (s[i+1]=='l'&&s[i+2]=='i' && s[i+3]=='c' && s[i+4]=='e' && !isalpha(s[i+5]) );
}
bool checkB(int i)//Bob
{
    if( length - i < 3 )    return false;
    return (s[i+1]=='o'&&s[i+2]=='b' && !isalpha(s[i+3]) );    
}

int main()
{
    scanf("%d",&k);
    getchar();
    gets(s);
    length = strlen(s);
    bob.push_back(-INF); 
    //bob位置在两端加上[-INF,INF] 统一操作 不需要判断下标是否越界 
    for( int i=0; i<length; i++ )
    {
        if( s[i]=='A' && checkA(i) )
        {
            alice.push_back(i);
            i += 5;
        }
        else if( s[i]=='B' && checkB(i) )    
        {
            bob.push_back(i);
            i += 3;
        }
    }
    bob.push_back(INF);
    //尺取法
    int lp = 0, rp = 0; //[lp,rp)内符合条件 
    ll ans = 0;
    for( int i=0, cize=alice.size(); i<cize; i++ )
    {
        while( alice[i] > bob[lp] + 3 + k )        lp++;
        while( bob[rp] <= alice[i] + 5 + k )    rp++;
        if( rp - lp >0 )    ans += rp - lp ;    
    } 
    printf("%lld\n",ans);
    return 0;
}

字串分值 

  对于一个字符串S,我们定义S的分值 f(S) 为S中七号出现一次的字符串个数。例如 f("aba") = 1,f("abc") = 3,f("aaa") = 0。

  现给定一个字符串S[ 0, 1, ..., n-1 ](长度为n),请你计算对于所有 S 的非空字串 S[ i, ..., j ](i<=j<n),f(S[ i, ..., j ])的和是多少。

  输入一行包含一个由小写字符组成的字符串S。


 

思路:

  最简单的思路即枚举所有字串,优化方案是固定 i ,每次递增 j ,并用一个哈希表判断新的字符是否出现过,时间复杂度O(n^2)。

  面临超时问题。观察字符串S = ababc

  字串        f值

  a           1

  ab            2

    aba          1

  abab        0 

  ababc        1

   b          1

   ba         2

   bab         1

   babc          2

     a           1

     ab         2

   abc         3

     b        1

     bc         2

       c            1

  其中加粗的表示分值项。按题意思路是求每行字符串的分值,不可避免的需要遍历所有字串。换个角度,最终目标是求分值和,我们可以求每个字符每列的分值。

如对第一个a,其分值是2。那么如何计算每个字符列分值和呢?观察第二个字符a,其计算分值的行是没有其他字符a的行,那么只要求其前一个a出现的位置 l 与后一个

a出现的位置 r ,那么该a的分值为( i - l )*( r - i )(其中 i 为这个a的下标,那么a的左边有 i - l种可能 × a右边有 r - i 种可能)。

  如何只看加粗字母的计数,题目思路可以视为从先行后列计数变为先列后行计数,从而只要记录相同字母第一次在其位置之前与之后的位置即可,跳出了遍历所有可能。

  (l-i) * (r-i)是因为对于第二个a,其左边界可以是ba、右边界可以是abc。

  注意用ans记录和时需要先把(l-i)转为long long,否则可能溢出,比如字母只出现在中间位置,则(1e5/2)*(1e5/2)对于int溢出。

实现代码:

#include<iostream>
#include<string>
using namespace std;

const int Max_N = 100000;

int last[26]; //last[i] : 字符 i+a 最近一次出现的位置 
int pre[Max_N]; //pre[i]:i左侧第一个与str[i]位置相同的位置 默认-1 
int nxt[Max_N]; //nxt[i]:i右侧第一个与str[i]位置相同的位置 默认 length(str长度) 

int main()
{
    string str;
    cin>>str;
    int length = str.length();
    
    /*求pre[]:默认值为-1*/
    for( int i=0; i<26; i++ )    last[i] = -1;
    for( int i=0; i<length; i++ )
    {
        int x   = str[i] - 'a';
        pre[i]  = last[x];
        last[x] = i;
    }
    /*求nxt[]:默认值为length*/
    for( int i=0; i<26; i++ )    last[i] = length;
    for( int i=length-1; i>=0; i-- )
    {
        int x   = str[i] - 'a';
        nxt[i]  = last[x];
        last[x] = i;
    }
    /*求解*/
    long long ans = 0;
    for( int i=0; i<length; i++ )
    {
        ans += (long long)(i-pre[i])*(nxt[i]-i);//!!
    }
    cout<<ans<<endl;
    return 0;
}

字串分值和

 

思路:

  与上一题类似,唯一变化的是 f 值的定义。仍然可以用求每个字符列值和的思路,稍有变化的是对于每个字符,其出现多次的分值为1而不是0。

比如aba,前一个a与后一个a总和分值为1,那么给哪一个字符加粗表示其为分值呢?我们可以认为规定一个:对于一个字串种多次出现的字符,我们

只给最左侧加粗即作为分值。

  那么对于每个字符,只需要考虑其左侧第一个与其相同的位置而不用考虑右侧,因为对于右侧它的分值都为1。

实现代码:

  

#include<iostream>
#include<string>
using namespace std;

const int Max_N = 100000;

int last[26]; //last[i] : 字符 i+a 最近一次出现的位置 
int pre[Max_N]; //pre[i]:i左侧第一个与str[i]位置相同的位置 默认-1  

int main()
{
    string str;
    cin>>str;
    int length = str.length();
    
    /*求pre[]:默认值为-1*/
    for( int i=0; i<26; i++ )    last[i] = -1;
    for( int i=0; i<length; i++ )
    {
        int x   = str[i] - 'a';
        pre[i]  = last[x];
        last[x] = i;
    }
    long long ans = 0;
    for( int i=0; i<length; i++ )
    {
        ans += (long long)(i-pre[i])*(length-i);
    }
    cout<<ans<<endl;
    return 0;
}

修改数组  https://www.dotcpp.com/oj/problem2301.html

给定一个长度为 N 的数组 A = [A1, A2, · · · AN ],数组中有可能有重复出现 的整数。

现在小明要按以下方法将其修改为没有重复整数的数组。小明会依次修改 A2,A3,··· ,AN。

当修改 Ai 时,小明会检查 Ai 是否在 A1 ∼ Ai−1 中出现过。如果出现过,则 小明会给 Ai 加上 1 ;如果新的 Ai 仍在之前出现过,小明会持续给 Ai 加 1 ,直 到 Ai 没有在 A1 ∼ Ai−1 中出现过。

当 AN 也经过上述修改之后,显然 A 数组中就没有重复的整数了。 现在给定初始的 A 数组,请你计算出最终的 A 数组。


思路

  一个简单的思路是模拟:用一个哈希表判断数子 A[ i ] 是否出现过,若出现则 A[ i ] 自增1。其问题仍然是时间复杂度:一个极端情况是A1到AN均为1,那么

时间复杂度有一次来到了O(n^2)。

  那么能否一步求得A[ i ]的值呢?考虑维护以及存在整数的区间,如[1,3 ],[ 17,25 ],[27,27]

  当处理Ai时:

    查找是否有区间覆盖A[ i ]

     若不存在,则A[ i ]不变

     若存在则将A[ i ] 加入集合

    将A[ i ]插入集合,处理可能导致的区间合并

  那么问题就变成如何实现维护区间的高效数据结构/算法,以及如何更新维护的区间。

具体实现

  可以用set<pair<int,int>>维护区间集合,其中pair<int,int>以左右区间端点形式表示区间。

  用upper_bound解决是否有区间覆盖A[ i ];而合并区间A,B可以先删去A,B,在加入A,B合并后的区间。

  因为set是用平衡树实现,所以插入删除操作时间复杂度O(logN),而查找upper_bound是用二分法实现,其时间复杂度也为O(logN),最终实现的时间复杂度为O(NlogN)。

实现代码

#include<algorithm>
#include<utility>
#include<cstdio>
#include<set>
using namespace std;

typedef set< pair<int,int> > set_pair;

const int INF   = 1e8; 
const int Max_N = 100000;

int a[Max_N];

int main()
{
    int N;
    scanf("%d",&N);
    for( int i=0; i<N; i++ )    scanf("%d",&a[i]);
    
    set_pair set_;
    set_.insert(make_pair(-INF,-INF));
    set_.insert(make_pair(INF,INF)); //作为区间的两端 统一算法 关键是第一个值 
    set_.insert(make_pair(a[0],a[0]));
    
    for( int i=1; i<N; i++ )
    {
        /* it.first是第一个>a[i]的区间  pre->first<=a[i](小于或等于)*/
        set_pair::iterator it = set_.upper_bound(make_pair(a[i],INF));
        set_pair::iterator pre = it; pre--;
        
        if( pre->second>=a[i] )
        {//有交集   否则pre->second<a[i]且pre->first<=a[i] 那么a[i]与pre无交集 
            a[i] = pre->second+1;//更新a[i] 
        }
        
        if( a[i]==pre->second+1 && a[i]==it->first-1 )
        {//合并两个区间 
            set_.insert(make_pair(pre->first,it->second));
            set_.erase(pre); set_.erase(it);
        }
        else if( a[i]==pre->second+1 )
        {//只与pre有交集 
            set_.insert(make_pair(pre->first,a[i]));
            set_.erase(pre);
        }    
        else if( a[i]==it->first-1 )
        {//只与it有交集 
            set_.insert(make_pair(a[i],it->second));
            set_.erase(it);
        }
        else
        {//不与先有区间有交集 
            set_.insert(make_pair(a[i],a[i]));
        }
    }
    
    for( int i=0; i<N; i++ )
    {
        printf("%d ",a[i]);
    }
    printf("\n");
    
    return 0;
}

   

posted @ 2021-03-14 16:25  代码改变头发  阅读(118)  评论(0编辑  收藏  举报