CUSTACM Summer Camp 2022 Training 2
A. awoo’s Favorite Problem
题意
给你两个字符串s与t(长度都为n),其中只有字符a、b、c
你可以执行以下操作任意次:
- 将ab改为ba
- 将bc改为cb
问能否将字符串s改为字符串t
数据范围: 1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1≤n≤105
tags:思维
思路
两种操作,无法就是:
- 将a后置
- 将c前置
如abc可以变为bac或acb,所有两种操作都不会改变a与c的相对位置,换句话说字符串s和t中的b去掉后两字符串应该是相等的,但这只是必要条件
观察下面去掉b后的字符串S’与T’,他们之中a与c的相对位置相同
- 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′
- 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之前,可能为以下情况
- 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
- 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'} as1′与at1′的相对位置相同,还需要保证在原来的串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
- 若一个数ai%m==0,则将其在数组中变为m个ai/m,其他数的相对位置不变
- 若有连续的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 1≤n+k≤2∗105,1≤ai,bi≤109
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 1≤x,n≤1000,0≤pos≤n−1
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),可以执行以下连续的操作
- 删除第i个字符(ai)
- 删除前缀相同的字符(排在前面的连续的字符,可以只有一个)
问把该字符串删除完的最多操作次数
数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 1 \leq n \leq 2*10^5 1≤n≤2∗105
tags:思维
思路
我们把每次操作的删除的字符个数称为代价,要想操作数最多,就需要每次操作的代价最小
-
当前缀长度x>1,选ai为前缀中的一个会使代价=x,因为你选其他的字符的代价一定为x+1
-
当前缀长度x=1时
-
不能删型如010中间的1,因为它会把非连续的0弄成连续的,而删除连续的多个字符只要一次操作,显然尽可能保持非连续的字符状态
-
于是我开始想的就是删最后一个字符,这样一定不会破坏非连续性,但是WA😭
-
其实还有更优方法且不破坏非连续性:把后面第一个连续的字符慢慢删除至数量=1
因为对于删除型如0111010
第一次选择删a0和a1,在来删除a2a3,操作数为2代价为4
而如果第一次选择删a0和a6,在来删a1a2a3,操作数为2代价为5
可以看出其中的区别了吧
而且注意是第一个!
-
细节
-
对于连续的字符,我们可以去用前缀和储存(如连续的字符只有1个也存)
-
然后用双指针去遍历前缀和即
-
遇到非连续的(即sum=1)就将right++
-
遇到第一个连续的就sum–(直到减到1)并将left++
-
如果left>right了,说明前缀就是连续的了,更新right=left
-
但是注意最后的(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 1≤n≤105,1≤l≤109
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 1≤l≤r≤n),删掉排列在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 1≤n≤250000,108≤m≤109
tags:排列组合,规律
思路
全排列的数量为n!,而n特别大了,我们不可能对每一个排列都单独讨论,可以猜测这题肯定是找规律,比竟数据太大了些。
思路1:找规律
打标找规律,先用暴力打标出来,然后找规律,只是我找不出来啊,然后看来题解才知道规律~~(话说这规律也太难发现了吧😭)~~
思路2:排列组合
首先对于子列的长度为 x = r − l + 1 x=r-l+1 x=r−l+1,它可以放在 n − ( x − 1 ) n-(x-1) n−(x−1)个位置上
对于每个子列的中的 S m a x − S m i n = r − l = x − 1 S_{max}-S_{min}=r-l=x-1 Smax−Smin=r−l=x−1可以直接算出它有 n − ( x − 1 ) n-(x-1) n−(x−1)种组合<Smax,Smin>
在子列中,不能出现比Smax大的和比Smin小的数,那么子列中的数只能为Smin,Smin+1,…,Smax共Smax-Smin+1=x个数,这些数可以任意排列有 x ! x! x!总组合
而对于子列之外的n-x个数可以任意排列,有 ( n − x ) ! (n-x)! (n−x)!总组合
所有完美只需枚举子列的长度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 2≤n≤100,2≤m≤106
tags:最短路径
思路
因为n比较小,可以先用Floyd-Warshall算法求出多源最短路径
对于求最短的子路径就是看 p 2 , . . . . , p m − 1 p_2,....,p_{m-1} p2,....,pm−1中哪些点可以删除,并且删除后不会改变原有路径(是否会出现更短路径)
例如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 1≤t≤100,1≤n≤109,3≤k≤109
tags:博弈论,找规律,思维
思路
这题可以达标找规律写出,但是说真的用逻辑完全想出真的难,我是想了很久😭
假设当前处于下表为x的格子,而且后面说的必败点是对先手而言的,且假设 k / 3 = j k/3=j k/3=j
-
假如只能走1、2
- 若 n % 3 = = 0 n \% 3==0 n%3==0,一定后手嬴,因为后者一定可以实现3步1轮(先手1后手2、先手2后手1)
- 若 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步
-
对于 k % 3 ≠ 0 k \% 3 \neq 0 k%3=0的情形,其实加上k对结果不影响,因为走k步就相当于走j轮3步+1步或2步,与上面那种情况相比无非就是可以走快点
-
对于 k % 3 = 0 k \% 3=0 k%3=0的情形,就比较麻烦了
- 若 x = k x=k x=k那么一定先手嬴
- 若 x < k x<k x<k那么可以按照情况1的步骤来判断
- 若
x
=
k
+
1
x=k+1
x=k+1,这时必败点
- 若先手走1,后手走k,后手嬴
- 若先手走k,后手走1,后手嬴
- 若先手走2,则 ( k − 1 ) % 3 = 2 ≠ 0 (k-1) \% 3=2\neq 0 (k−1)%3=2=0后手走2,就到了情况1中 x % 3 = 0 x \% 3=0 x%3=0的情形了,即到了必败点,后手嬴
-
最后经过不断不断的思考得出重要结论:必败点+3一定还是为必败点
-
若x<k,毫无疑问这时正确的,因为无疑0是最开始的必败点而这又属于第1种情况,0、3、6、9……这些数%3=0属于必败点
-
若x>k时,这时可以走k步了,对于k%3!=0就是第一种情况我们不说明,而对于k%3=0时,我们把k+1看作最开始的必败点(上面已经说明了)。
- k+1+3也是必败点
- 先手走1后手走2,走到临近必败点(k+1)
- 先手走2后手走1,走到临近必败点(k+1)
- 先手走k后手走1,走到跨区间必败点(3)
- k+1+3+3也是必败点
- 同上面先手走1、2时可以走到临近必败点(k+1+3)
- 先手走k后手走1,走到跨区间必败点(6)
规律:
- 每一个长度为k+1的区间(0-k,k+1-2 k+1……)都是必败点都是一模一样的情形。*
- 从每个区间的起始必败点开始(0、k+1……),+3,若先手走1、2可以实现走3步回到临近必败点,若先手走k可以实现走k+1步回到跨区间必败点,所以必败点+3一定是必败点
- 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 1≤n≤10,1≤m≤10
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(t∗26∗m∗n)
J. Shortest and Longest LIS
题意
给你一个有n个元素的排列 A 中,每相邻两个元素的大小关系,要你构造出两组解,使得第一组解的 LIS 最短,第二组解的 LIS 最长。
数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 1 \leq n \leq 2*10^5 1≤n≤2∗105
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)
思维
对于题目要求构造出最优解,可以先从极限开始(无限制条件的最优解)开始,更具构造条件(限制条件)来一步一步降低最优解的最优性