CUSTACM Summer Camp 2022 Training 2(10题)

CUSTACM Summer Camp 2022 Training 2

A. awoo’s Favorite Problem

题意

给你两个字符串s与t(长度都为n),其中只有字符a、b、c

你可以执行以下操作任意次:

  1. 将ab改为ba
  2. 将bc改为cb

问能否将字符串s改为字符串t

数据范围: 1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1n105

tags:思维

思路

两种操作,无法就是:

  1. 将a后置
  2. 将c前置

如abc可以变为bac或acb,所有两种操作都不会改变a与c的相对位置,换句话说字符串s和t中的b去掉后两字符串应该是相等的,但这只是必要条件

观察下面去掉b后的字符串S’与T’,他们之中a与c的相对位置相同

  1. S’: a s 1 ′ , a s 2 ′ , c s 3 ′ , c s 2 ′ a_{s1'},a_{s2'},c_{s3'},c_{s2'} as1,as2,cs3,cs2
  2. T’: a t 1 ′ , a t 2 ′ , c t 3 ′ , c t 2 ′ a_{t1'},a_{t2'},c_{t3'},c_{t2'} at1,at2,ct3,ct2

但是在未去b之前,可能为以下情况

  1. S: b s 1 , a s 2 , a s 3 , c s 4 , c s 5 , b s 6 b_{s1},a_{s2},a_{s3},c_{s4},c_{s5},b_{s6} bs1,as2,as3,cs4,cs5,bs6
  2. T: a t 1 , b t 2 , a t 3 , c t 4 , b t 5 , c t 6 a_{t1},b_{t2},a_{t3},c_{t4},b_{t5},c_{t6} at1,bt2,at3,ct4,bt5,ct6

发现这样是不能将S变为T的,原因出在外面的操作只能将a后置、c前置

对于S’与T’中,即使保证了 a s 1 ′ 与 a t 1 ′ a_{s1'}与a_{t1'} as1at1的相对位置相同,还需要保证在原来的串S与T中as1出现的位置不落后于at1,cs1出现的位置不超前于ct1

代码

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

int n;
string s,t;

void solve(){
    string temp1="",temp2="";
    priority_queue<int>sa,sc,ta,tc;//优先队列,存放a、c在S、T中的位置,其实不用这个也可以,因为遍历就是从小到大遍历的
    //遇到b就不要将其加入temp1,temp2了,就可以实现去掉S、T中的b了
    for(int i=0;i<n;i++){
        if(s[i]=='a'){
            sa.push(i);
            temp1+=s[i];
        }
        if(s[i]=='c'){
            sc.push(i);
            temp1+=s[i];
        }
        if(t[i]=='a'){
            ta.push(i);
            temp2+=t[i];
        }
        if(t[i]=='c'){
            tc.push(i);
            temp2+=t[i];
        }
    }
    //必要条件temp1==temp2
    if(temp1!=temp2){
        cout<<"NO"<<endl;
        return;
    }
    //对与a的相对位置判断
    while(!sa.empty()){
        if(sa.top()>ta.top()){
            cout<<"NO"<<endl;
            return;
        }
        sa.pop();ta.pop();
    }
    //对于c的相对位置判断
    while(!sc.empty()){
        if(sc.top()<tc.top()){
            cout<<"NO"<<endl;
            return;
        }
        sc.pop();tc.pop();
    }
    cout<<"YES"<<endl;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int x;
    for(cin>>x;x;x--){
        cin>>n>>s>>t;
        solve();
    }
}

复杂度

O ( n ) O(n) O(n)

思维

对于操作类题型,可以去尝试着把其操作抽象出现,也就是分析其操作产生的实质效果,在根据抽象出的作用分析答案

在本题中,对操作的抽象就是将a后置,将c前置


B. Fishingprince Plays With Array

题意

给你两个数组A[a1,a2,…,an]与B [b1,b2,…,bk],判断通过以下操作能否把A变为B(可以操作任意次)

给定一个数m

  1. 若一个数ai%m==0,则将其在数组中变为m个ai/m,其他数的相对位置不变
  2. 若有连续的m个相同的数,将这m个数变为1个数ai*m,其他数的相对位置不变

数据范围: 1 ≤ n + k ≤ 2 ∗ 1 0 5 , 1 ≤ a i , b i ≤ 1 0 9 1 \leq n+k \leq 2*10^5,1\leq a_i,b_i \leq 10^9 1n+k2105,1ai,bi109

tags:思维

思路

可以观察到这两种操作是互逆的。即如果可以通过一系列操作operationA变为B,也可以通过~operation把B变为A

但是我们不能简简单单的就直接去硬把A变为B或B变为A,因为有两个操作,它们操作顺序无规律可循

但是由于两种操作可逆,我们可以把A变为C,相应的也会有方法把B变为C(如果A可以变为B)

那么我们就可以用这个中间变量C,在某一极限下,C的值是一定的,判断AB的极限是否同为C即可

而该极限就是1.一直进行操作1直到不能在分为止、2.一直进行操作2直到不能在合为止

但是对于操作2我们很难实现,因为对于操作2要求连续m个相同的数,我们想要到极限可能需要中途进行操作1:

如1,4,8,4(m=2)不能在进行操作2了,但是他的极不是这个,而是将8拆为44在合形成的1,16

而对于操作1每一个数可不可分是确定的,分成的最小单位也是确定的,所以我们的想法是一直进行操作1直到不能在分为止

细节

对于拆分数,我们不能把它存放在数组中,看数据范围可以直到根本存不下

我们只需记录它被拆成数x有count个就行了(pair<int,int>P,P.first=x,P.second=count),如何按顺序匹配P就行了

而且还有细节,当前后两个数拆成的数相同,即P1.first==P2.first,我们需要将它们合并,不然会把本应正确的匹配当成错误的了

最后:count开long long!!!

代码

#include<iostream>
#include<vector>
#include<utility>
#define ll long long
using namespace std;

typedef pair<int,ll>P;
const int maxn=5e4+5;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        int n,m;
        cin>>n>>m;
        vector<P>a,b;
        for(int i=0;i<n;i++){
            int x;
            ll count=1;
            cin>>x;
            while(x%m==0){//一直执行操作1直到不能拆为止
                count*=m;//记录拆的格个数
                x/=m;//记录最后拆成的数
            }
            if(a.empty()||a.back().first!=x)a.push_back(P(x,count));//若出现前后拆成的数相同需要合并
            else a.back().second+=count;
        }
        int k;cin>>k;
        for(int i=0;i<k;i++){
            int x;
            ll count=1;
            cin>>x;
            while(x%m==0){//同拆A一样去拆B
                count*=m;
                x/=m;
            }
            if(b.empty()||b.back().first!=x)b.push_back(P(x,count));
            else b.back().second+=count;
        }
        if(a==b)cout<<"Yes"<<endl;//判断AB的极限是否相同
        else cout<<"No"<<endl;
    }
}

复杂度

O ( ≤ n l o g n ) O(\leq nlogn) O(nlogn)

思维

对于两种互逆的操作,去寻找一个中间变量(最好是”极限“,即直到不能执行某操作位置),如何通过该中间变量来判断两变量的关系


C. Binary Search

题意

在这里插入图片描述

题目给定了二分排序算法的过程,但是它只对有序的数列才有用。对于长度为n的序列,它能在nlogn的时间内找到x的位置。

有个大聪明认为它可以用于无序的排列。找出长度为n的排列(其中的数从1到n),用二分法找出x的位置,该位置与指定的位置pos相同,问这种排列有多少个,结果mod 1e9+7

数据范围: 1 ≤ x , n ≤ 1000 , 0 ≤ p o s ≤ n − 1 1 \leq x,n \leq 1000,0 \leq pos \leq n-1 1x,n1000,0posn1

tags:构造、模拟

思路

模拟二分法的过程,构造特定的值,让middle朝着我们希望的pos移动,在其中利用排列组合来计算有多少种可行的排列

下面这个例子是我设置好了的,把所有过程都考虑进去了,并在其中计算了排列的种数

例如:n=6,x=3,pos=4

其中比x小的数有small个,比x大的有big个

因为最终要让a[left-1]==x也是就说最后的a[middle]=x是终止二分,这样才会有left=middle+1

对于第一次搜索left=0,right=6,middle=3<pos=4,为了让middle往pos靠,我们需要让left右移,也就是把a[middle]设置为小于x(a[middle]有small种可能,之后更新small–以便下次使用。因为我们是构造特定的值,所以使a[middle]=-1(无限小就行)),这样left=middle+1=4

对于第二次搜索,left=4,right=6,middle=5>pos=5,为了让middle往pos靠,我们需要人right左移,也就是把a[middle]设置为大于x(a[middle]有big种可能,之后更新big–以便下次使用。因为我们是构造特定的值,所以使a[middle]=1e9(无穷大就行)),这样right=middle=5

对于第三次搜索,left=4,right=5,middle=4=pos=4,这时我们直接让a[middle]=x,就会执行left=middle+1并跳出循环,并且之后一定判断a[left-1]=x为true

而剩下的big+small个数可以随便排,有(big+small)!种可能

代码

#include<iostream>
#define ll long long
using namespace std;

const int mod=1e9+7,maxn=1005;
int n,x,pos;
int a[maxn];
int small,big;
ll ans=1;

bool erfen(){
    int left=0,right=n;
    while(left<right){
        int middle=(left+right)>>1;
        if(middle>pos){//middle>pos,为了让midllle往pos靠,a[middle]设置为无穷大
            a[middle]=maxn;
            ans=(ans*big)%mod;//a[middle]有big种选择
            big--;//更新big
        }
        else if(middle<pos){//middle<pos,为了让middle往pos靠,a[middle]设置为无穷小
            a[middle]=-1;
            ans=(ans*small)%mod;//a[middle]有small种选择
            small--;//更新samll
        }
        else a[middle]=x;//middle=pos,直接让a[middle]=x
        if(a[middle]<=x){
            left=middle+1;
        }
        else right=middle;
    }
    if(left>0&&a[left-1]==x)return true;//按照上面的模拟和构造,最后一定为true
    else return false;
}


int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>x>>pos;
    small=x-1,big=n-x;
    erfen();
    for(int i=small+big;i;i--)ans=(ans*i)%mod;//剩下的small+big个数随意排,种数为(small+big)!
    cout<<ans<<endl;
}

复杂度

O ( n l o g n ) O(nlogn) O(nlogn)

思维

对于给你一串代码,按照所给代码解决相关问题的题,不要被代码牵着走,而要牵着代码走,通过==构造特殊的数据使代码的运行方向按照我们预期的方向走==


D. String Deletion

题意

给你一个长度为n的字符串(只有字符0、1),可以执行以下连续的操作

  1. 删除第i个字符(ai
  2. 删除前缀相同的字符(排在前面的连续的字符,可以只有一个)

问把该字符串删除完的最多操作次数

数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 1 \leq n \leq 2*10^5 1n2105

tags:思维

思路

我们把每次操作的删除的字符个数称为代价,要想操作数最多,就需要每次操作的代价最小

  1. 当前缀长度x>1,选ai为前缀中的一个会使代价=x,因为你选其他的字符的代价一定为x+1

  2. 当前缀长度x=1时

    1. 不能删型如010中间的1,因为它会把非连续的0弄成连续的,而删除连续的多个字符只要一次操作,显然尽可能保持非连续的字符状态

    2. 于是我开始想的就是删最后一个字符,这样一定不会破坏非连续性,但是WA😭

    3. 其实还有更优方法且不破坏非连续性:把后面第一个连续的字符慢慢删除至数量=1

      因为对于删除型如0111010

      第一次选择删a0和a1,在来删除a2a3,操作数为2代价为4

      而如果第一次选择删a0和a6,在来删a1a2a3,操作数为2代价为5

      可以看出其中的区别了吧

      而且注意是第一个!

细节

  1. 对于连续的字符,我们可以去用前缀和储存(如连续的字符只有1个也存)

  2. 然后用双指针去遍历前缀和

    1. 遇到非连续的(即sum=1)就将right++

    2. 遇到第一个连续的就sum–(直到减到1)并将left++

    3. 如果left>right了,说明前缀就是连续的了,更新right=left

    4. 但是注意最后的(num-l+2)/2是表达什么意思:因为上面的操作都是按照上面的分析一步一步进行,所以操作数ans++,但是最后的left到num的几个非连续的数不会计算在其中,其还需要(num-l+1+1)/2次操作

      如011101010

      最后的非连续序列(01010)会一直执行while(r<=num&&sum[r]==l)r++直到跳出循环,不会记录操作数ans

代码

#include<iostream>
using namespace std;

int n;
string s;

int main(){
    int t;
    for(cin>>t;t;t--){
        cin>>n>>s;
        int cnt=1,ans=0,sum[500005],num=0;
        for(int i=1;i<n;i++){
            if(s[i]==s[i-1])cnt++;
            else{
                sum[num++]=cnt;
                cnt=1;
            }
        }
        sum[num]=cnt;
        int l=0,r=0;
        while(1){
            while(r<=num&&sum[r]==1)r++;
            if(r>num)break;
            sum[r]--;
            l++;
            ans++;
            if(l>r)r=l;
        }
        cout<<(ans+(num-l+2)/2)<<endl;//位运算一定要加括号
    }
}

复杂度

O ( n ) O(n) O(n)


E. Discrete Acceleration

题意

有一条长度为l米的路,开始的坐标为0,终点的坐标为l,一辆车在开头,一辆车在终点,二者以1米每秒的速度相向而行

在这条路上有n个路标,如果车经过一个路标,则它的速度+1

问多久这两辆车能够相遇?(精度为1e-6)

数据范围: 1 ≤ n ≤ 1 0 5 , 1 ≤ l ≤ 1 0 9 1 \leq n \leq 10^5,1 \leq l \leq 10^9 1n105,1l109

tags:浮点数二分

思路

很明显满足二分条件,但是注意是二分精度为1e-6

代码

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

const int maxn=1e5+5;
int n,l;
int a[maxn];

bool erfen(double t){
    double s1=1,p1=0,t1=t;
    for(int i=1;i<=n;i++){
        if(p1>=l)return true;//若走到顶了肯定是相遇了
        if(t1>=(a[i]-p1)/s1){//判断剩下的时间是否可以经过下一个路标
            t1-=(a[i]-p1)/s1;//如果可以这减去这一段路程所用的时间
            p1=a[i];//定位最新位置
            s1++;//速度++
        }
        else break;
    }
    p1+=t1*s1;//若剩下的时间不能在经过落标了,求剩下时间可以走的距离
    //求另一辆车的方法类似,只是从终点开始
    double s2=1,p2=l,t2=t;
    for(int i=n;i>=1;i--){
        if(p2<=0)return true;
        if(t2>=(p2-a[i])/s2){
            t2-=(p2-a[i])/s2;
            s2++;
            p2=a[i];
        }
        else break;
    }
    p2-=t2*s2;
    return p1>=p2;//若p1>=p2说明经过该时间两辆车可以相遇
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        cin>>n>>l;
        for(int i=1;i<=n;i++)cin>>a[i];
        double l=0,r=1e9;
        while(r-l>1e-6){//二分精度为1e-6
            double mid=(l+r)/2;
            if(erfen(mid))r=mid;
            else l=mid;
        }
        cout<<setprecision(6)<<setiosflags(ios::fixed)<<l<<endl;//输入l或r都行
    }
}

复杂度

O ( n l o g l ) O(nlogl) O(nlogl)

思维

尽量让自己的思维往二分的想法上靠,二分真的很好用


F. New Year and Permutation

题意

给你两个数n,m。

对于一个长度为n排列,包含数字1到n,找出l与r( 1 ≤ l ≤ r ≤ n 1 \leq l \leq r \leq n 1lrn),删掉排列在l左边和在r右边的数(不包括l、r本身)得到其子列S。若Smax-Smin=r-l这它是完美的子列(Smax是子列中最大的数,同理Smin)。而完美的子列个数为这个排列的happiness值

求长度为n的所有排列的happiness的总和且mod m

数据范围: 1 ≤ n ≤ 250000 , 1 0 8 ≤ m ≤ 1 0 9 1 \leq n \leq 250000,10^8 \leq m \leq 10^9 1n250000,108m109

tags:排列组合,规律

思路

全排列的数量为n!,而n特别大了,我们不可能对每一个排列都单独讨论,可以猜测这题肯定是找规律,比竟数据太大了些。

思路1:找规律

打标找规律,先用暴力打标出来,然后找规律,只是我找不出来啊,然后看来题解才知道规律~~(话说这规律也太难发现了吧😭)~~

在这里插入图片描述

思路2:排列组合

首先对于子列的长度为 x = r − l + 1 x=r-l+1 x=rl+1,它可以放在 n − ( x − 1 ) n-(x-1) n(x1)个位置上

对于每个子列的中的 S m a x − S m i n = r − l = x − 1 S_{max}-S_{min}=r-l=x-1 SmaxSmin=rl=x1可以直接算出它有 n − ( x − 1 ) n-(x-1) n(x1)种组合<Smax,Smin>

在子列中,不能出现比Smax大的和比Smin小的数,那么子列中的数只能为Smin,Smin+1,…,Smax共Smax-Smin+1=x个数,这些数可以任意排列有 x ! x! x!总组合

而对于子列之外的n-x个数可以任意排列,有 ( n − x ) ! (n-x)! (nx)!总组合

所有完美只需枚举子列的长度x就行了

细节

对于阶乘的计算先把1到250000的阶乘全算出来在储存以后直接O(1)时间查找,不要傻的边算边求

而且由于数据真的大,尽量在每计算一次就mod一次,防止溢出

代码

#include<iostream>
#define ll long long
using namespace std;

int main(){
        ll n,mod;
        cin>>n>>mod;
        ll a[250005]={1};
        for(int i=1;i<=250000;i++)a[i]=(a[i-1]*i)%mod;//计算阶乘并且存在数组中
        ll ans=0;
        for(int i=1;i<=n;i++){
            ans=(ans+(n-i+1)%mod*(n-i+1)%mod*a[i]%mod*a[n-i]%mod)%mod;
        }
        cout<<ans<<endl;
}

复杂度

O ( n ) O(n) O(n)


G. Anna, Svyatoslav and Maps

题意

给你一个有n个顶点的有向无权图(邻接矩阵形式),不含环

给你一条有m个顶点的路径 p 1 , p 2 , . . . . , p m p_1,p_2,....,p_m p1,p2,....,pm,要你求它的一条子路径 v 1 , v 2 , . . . , v k v_1,v_2,...,v_k v1,v2,...,vk其中 v 1 = p 1 , v k = p m v_1=p_1,v_k=p_m v1=p1,vk=pm,保证原路径是按顺序通过该子路径的最短路径,求最短的子路径

数据范围: 2 ≤ n ≤ 100 , 2 ≤ m ≤ 1 0 6 2 \leq n \leq 100,2 \leq m \leq 10^6 2n100,2m106

tags:最短路径

思路

因为n比较小,可以先用Floyd-Warshall算法求出多源最短路径

对于求最短的子路径就是看 p 2 , . . . . , p m − 1 p_2,....,p_{m-1} p2,....,pm1中哪些点可以删除,并且删除后不会改变原有路径(是否会出现更短路径)

在这里插入图片描述

例如m=4,路径为1234

如果删除2,那么遍历134的最短路劲不是1234而是134,不可删除

如果删除3,遍历124的最短路径还是1234,可以删除

只需要从p1到pm以三个点为循环来判断中间的那个点删除会不会改变原有路径

dis[one][three]<dis[one][two]+dis[two][three]那么说明删除节点two会导致原有最短路径改变,否则可以删除

代码

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

const int maxn=105,inf=1e9,maxm=1e6+5;
char g[maxn][maxn];
int dis[maxn][maxn];
int n;


void flyod(){//Flyod算法求多源最短路径
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(g[i][j]=='1')dis[i][j]=1;
            else if(j==i)dis[i][j]=0;
            else dis[i][j]=inf;
    }
    }
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                if(dis[i][j]>dis[i][k]+dis[k][j])dis[i][j]=dis[i][k]+dis[k][j];
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>g[i][j];
        }
    }
    flyod();
    int m,a[maxm];
    cin>>m;
    for(int i=0;i<m;i++)cin>>a[i];
    vector<int>v;
    v.push_back(a[0]);//先把起点加入到子路径中
    int one=a[0],two=a[1];//初始时判断的三个点为a[0],a[1],a[2]
    for(int i=2;i<m;i++){
        int three=a[i];
        if(dis[one][three]<dis[one][two]+dis[two][three]){
            v.push_back(two);//不能删,就把two加入的子路径中
            one=two;//更新one、two、three,各自往后移1个单位
            two=three;
        }
        else {//否则可以删two
            two=three;//这时新的三个点中只用tow,three往后移1个单位
        }
    }
    v.push_back(a[m-1]);//终点加入
    cout<<v.size()<<endl;
    for(int i=0;i<v.size();i++)cout<<v[i]<<' ';
    cout<<endl;
}

复杂度

o ( n 3 ) o(n^3) o(n3)


H. 1-2-K Game

题意

有一个纸条,纸条上有n+1个格子,下标从0开始;现在在最后一个格子(即下标为n),两个人轮流进行操作,每次操作可以向前移动1,2,或者k格,现在给定n和k,问你是先手赢还是后手赢

数据范围: 1 ≤ t ≤ 100 , 1 ≤ n ≤ 1 0 9 , 3 ≤ k ≤ 1 0 9 1 \leq t \leq 100,1 \leq n \leq 10^9,3 \leq k \leq 10^9 1t100,1n109,3k109

tags:博弈论,找规律,思维

思路

这题可以达标找规律写出,但是说真的用逻辑完全想出真的难,我是想了很久😭

假设当前处于下表为x的格子,而且后面说的必败点是对先手而言的,且假设 k / 3 = j k/3=j k/3=j

  1. 假如只能走1、2

    1. n % 3 = = 0 n \% 3==0 n%3==0,一定后手嬴,因为后者一定可以实现3步1轮(先手1后手2、先手2后手1)
    2. n % 3 ≠ 0 n \% 3 \neq 0 n%3=0,一定先手嬴,因为先手可以先走余数,然后就回到第一种情况了

    可以得出:只能走1、2步时, n % 3 = = 0 n \% 3==0 n%3==0必败点。(当加上可以走k步时,若x<k时就是只能走1、2步)

    并且明白其核心步骤是保证每一轮走3步

  2. 对于 k % 3 ≠ 0 k \% 3 \neq 0 k%3=0的情形,其实加上k对结果不影响,因为走k步就相当于走j轮3步+1步或2步,与上面那种情况相比无非就是可以走快点

  3. 对于 k % 3 = 0 k \% 3=0 k%3=0的情形,就比较麻烦了

    1. x = k x=k x=k那么一定先手嬴
    2. x < k x<k x<k那么可以按照情况1的步骤来判断
    3. x = k + 1 x=k+1 x=k+1,这时必败点
      1. 若先手走1,后手走k,后手嬴
      2. 若先手走k,后手走1,后手嬴
      3. 若先手走2,则 ( k − 1 ) % 3 = 2 ≠ 0 (k-1) \% 3=2\neq 0 (k1)%3=2=0后手走2,就到了情况1中 x % 3 = 0 x \% 3=0 x%3=0的情形了,即到了必败点,后手嬴
  4. 最后经过不断不断的思考得出重要结论:必败点+3一定还是为必败点

    1. 若x<k,毫无疑问这时正确的,因为无疑0是最开始的必败点而这又属于第1种情况,0、3、6、9……这些数%3=0属于必败点

    2. 若x>k时,这时可以走k步了,对于k%3!=0就是第一种情况我们不说明,而对于k%3=0时,我们把k+1看作最开始的必败点(上面已经说明了)。

      1. k+1+3也是必败点
        1. 先手走1后手走2,走到临近必败点(k+1)
        2. 先手走2后手走1,走到临近必败点(k+1)
        3. 先手走k后手走1,走到跨区间必败点(3)
      2. k+1+3+3也是必败点
        1. 同上面先手走1、2时可以走到临近必败点(k+1+3)
        2. 先手走k后手走1,走到跨区间必败点(6)

      规律:

      1. 每一个长度为k+1的区间(0-k,k+1-2 k+1……)都是必败点都是一模一样的情形。*
      2. 从每个区间的起始必败点开始(0、k+1……),+3,若先手走1、2可以实现走3步回到临近必败点,若先手走k可以实现走k+1步回到跨区间必败点,所以必败点+3一定是必败点

说了这么多,不知道我有没有表达清楚我的意思,唉,好好想想吧,但是好像意义不大😢

所以对于k%3=0的情形我们就可以直接n%(k-1)回到第一个区间来讨论

代码

#include <bits/stdc++.h>
using namespace std;
int main(){
	int t;
	cin >> t;
	while(t--){
		int n, k;
		cin >> n >> k;
		if(k % 3 == 0) n%=k+1;
		if(n % 3 == 0 && n != k){
			cout << "Bob" << endl; 
			continue;
		}
		cout << "Alice" << endl;
	}
}

复杂度

O ( 1 ) O(1) O(1)


I. Spy-string

题意

让你构造一个长度为m,和n个字符串都只有【最多一个地方】不同的字符串

数据范围: 1 ≤ n ≤ 10 , 1 ≤ m ≤ 10 1 \leq n \leq 10,1 \leq m \leq 10 1n10,1m10

tags:暴力

思路

看到数据范围毫无疑问就是暴力

但是不要傻的直接去对每个位置讨论26个字母,单论情况就有26m了,T吧

若以一个字符串为基准来改,每个位置有26种情况而其余位置不变,只有26*m种情况了

代码

#include <iostream>
using namespace std;

int n, m;
string str[100];
string s;

bool judge(string x)
{
    for (int i = 1; i < n; i++)
    {
        int count = 0;
        for (int j = 0; j < m; j++)
        {
            if (x[j] != str[i][j])
                count++;
            if (count > 1)//count为不一样的个数,若超过1个就false了
                return false;
        }
    }
    return true;
}

void solve()
{
    s = str[0];
    for (int i = 0; i < m; i++)
    {
        for (int j = 'a'; j <= 'z'; j++)
        {
            s[i] = j;
            if (judge(s))
            {
                cout << s << endl;
                return;
            }
        }
        s = str[0];//还原字符串继续讨论
    }
    cout << -1 << endl;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for (cin >> t; t; t--)
    {
        cin >> n >> m;
        for (int i = 0; i < n; i++)
            cin >> str[i];
        solve();
    }
}

复杂度

O ( t ∗ 26 ∗ m ∗ n ) O(t*26*m*n) O(t26mn)


J. Shortest and Longest LIS

题意

给你一个有n个元素的排列 A 中,每相邻两个元素的大小关系,要你构造出两组解,使得第一组解的 LIS 最短,第二组解的 LIS 最长。

数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 1 \leq n \leq 2*10^5 1n2105

tags:构造

思路

对于从1到n的全排列,我们先不管限制条件,最短的LIS是n,n-1,…,2,1,最长的LIS是1,2,…,n-1,n

就算加了限制条件,最短与最长的LIS也一定会从上面两个极限通过改变个别数的顺序而来

在求最短LIS时,遇到<我们就要改变ai与ai+1原来的位置了

同理,在求最长LIS时,遇到>我们就要改变ai与ai+1原来的位置了

注意有连续的限制如<<<时是翻转ai,ai+1,ai+2,ai+3,用reverse函数可以简单的实现,但是注意其范围reverse[i,i+4)

代码

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

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int t;
    for(cin>>t;t;t--){
        int n;
        string s;
        int a[200005];
        cin>>n>>s;
        for(int i=1;i<=n;i++)a[i]=n-i+1;//预定义最短LIS
        for(int i=0;i<n-1;i++){
            int index=i;
            while(i<n-1&&s[i]=='<')i++;//若出现<就得改变该数的顺序了
            if(index!=i)reverse(a+index+1,a+i+2);//注意我们数组a与字符串数组的下表不是从同一初值开始的,注意范围
        }
        for(int i=1;i<=n;i++)cout<<a[i]<<' ';//预定义最长LIS
        cout<<endl;
        for(int i=1;i<=n;i++)a[i]=i;
        for(int i=0;i<n-1;i++){
            int index=i;
            while(i<n-1&&s[i]=='>')i++;//若出现>就得改变该数的顺序了
            if(index!=i)reverse(a+index+1,a+i+2);
        }
        for(int i=1;i<=n;i++)cout<<a[i]<<' ';
        cout<<endl;
    }
}

复杂度

O ( n ) O(n) O(n)

思维

对于题目要求构造出最优解,可以先从极限开始(无限制条件的最优解)开始,更具构造条件(限制条件)来一步一步降低最优解的最优性

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值