题目链接:仪表板 - 教育代码部队第 152 轮(评级为 2 区) - 代码部队 (codeforces.com)
目录
B. Monsters
一、题目描述
Monocarp正在玩另一款电脑游戏。再一次,他的角色杀死了一些怪物。游戏中有n个怪物,编号从1到n,其中第i个怪物最初拥有ai生命值。
Monocarp的角色有一个技能,对当前生命值最高的怪物造成k点伤害。如果有几个,则选择索引较小的那个。如果怪物的生命值在Monocarp使用技能后小于等于0,那么它就会死亡。
Monocarp使用他的能力,直到所有的怪物死亡。你的任务是决定怪物死亡的顺序。
输入
第一行包含一个整数t(1≤t≤10^4)——测试用例的数量。
每个测试用例的第一行包含两个整数n和k(1≤n≤3⋅10^5;1≤k≤10^9)-怪物的数量和怪物技能造成的伤害。
第二行包含n个整数a1,a2,…,an(1≤ai≤109)-怪物的初始生命值。
所有测试用例的总和不超过3⋅105。
输出
对于每个测试用例,打印n个整数——怪物的索引,按照它们死亡的顺序。
测试样例:
输入
3
3 2
1 2 3
2 3
1 1
4 3
2 8 3 5
输出
2 1 3
1 2
3 1 2 4
二、解题思路
因为数据 t <= 1e4, n <= 3e5;所以读入数据的时间复杂度为 n*t <= 3e9 ;
因为有t组测试用例,所以在每组数据中,时间复杂度只能为O(n)才能通过,当数据不取极限值的时候,每组数据的时间复杂度可以为O(n*logn);
情况1: 假设只有两个小怪物,并且他们的血量a1,a2“都是”k的整数倍,开始的时候肯定会对其中一个血量高的进行伤害,当进行多次伤害后,他们的血量会变得一样,再经过多次后,他们的血量都会变成k,所以此时肯定会对1号小怪物进行伤害,因为它的序号低,所以1号小怪物先被杀死,死亡序列一定是1号小怪在2号小怪的前面(1, 2 )。
同理,当有多个小怪物的血量都为k的整数倍时,一定是序号小的那个小怪先被杀死;
情况2.假设只有两只小怪物,并且他们的血量a1,a2“都不是”k的整数倍,开始的时候肯定会对其中一个血量高的进行伤害,当进行多次伤害后,他们的血量肯定都会小于k并且大于0,假设多次伤害后的血量为a1=2,a2=3,假设k=4,此时肯定会对血量高的小怪进行伤害,所以2号小怪物先被杀死,死亡序列一定是2号小怪在1号小怪的前面(2, 1 )。
所以,当两个小怪的物的血量都不为k的整数倍时,一定对k取余后,余数大的那个小怪先被杀死;
情况3.假设只有两只小怪物,并且他们的血量a1是k的整数倍,a2不是k的整数倍,开始的时候肯定会对其中一个血量高的进行伤害,当进行多次伤害后,a1会变成k,a2会变成一个小于k的数并且大于0的数,假设a2变成k-1,所以1号小怪物先被杀死,死亡序列一定是1号小怪在2号小怪的前面(1, 2 )。
所以,当两个小怪的物的血量一个为k的整数倍,另一个不是时,一定是k的整数倍的那个,先被杀死;可以理解为,两者都对k取余后,余数小的那个小怪先被杀死;
综上所述:小怪的血量只与自身的序号和对k取余后的值有关;所以,可以将小怪的血量同时对k进行取余。但是,都进行取余后,第二种情况和第三种明显相悖,所以要进行稍微的转换:血量ai对k取余时,如果血量为k的整数倍时,将血量ai变为k;在情况3中,a1,a2对取余后,a1变成k,a2变成k-1,a1先被杀死。所以情况2和情况3都得出结论:对ai对k取余,余数不同时,余数小的先被杀死;情况1得出结论,ai对k取余,余数相同时,序号小的先被杀死。
所以,解题思路为,用一个结构体存储每个小怪物的血量和序号,在读入小怪物的血量后,将血量对k进行取余,血量为k的整数倍(对k取余为0)的,血量变为k。
之后,对整个结构体数组进行排序,如果两个小怪物的血量相同,序号小的排在前面,血量不同时,血量大的先被杀死。(代码中布尔型的cmp函数的含义)
可以直接调用sort函数,并且自己手写一个排序的cmp函数,时间复杂度大概为O(n*logn),因为此题的数据并不是极限值,所以勉强可以通过。
三、代码实现
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include<iostream>
#include<algorithm>
using namespace std;
typedef struct edges
{
int xv;
int zhi;
}edges;
int n,k ;
bool cmp(edges A,edges B )
{
if( A.zhi != B.zhi ) return A.zhi > B.zhi;
return A.xv < B.xv;
}
int main( )
{
int t;
scanf("%d",&t);
while(t-- )
{
scanf("%d%d",&n,&k );
edges a[n*2];
for(int i=1;i<=n;i++ )
{
scanf("%d",&a[i].zhi );
a[i].zhi%=k;
if(a[i].zhi ==0 ) a[i].zhi = k;
a[i].xv=i;
}
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++ )
printf("%d ",a[i].xv);
puts("");
}
return 0;
}
四、总结
做题之前先进行时间复杂度的分析,在确定自己思路的时间复杂度正确之后,再进行代码的实现。只前用的优先队列和set存结构体进行排序,但是算法的时间复杂度为O( k ),当k过大时,会超时,但是是当代码实现之后,才发现会超时,导致做题和补题浪费了大量的时间。
C. Binary String Copying
一、题目描述
给定一个由n个字符0和/或1组成的字符串s。
把这个字符串复制m个,第i个拷贝是字符串ti。
然后对每个副本只执行一个操作:对于第i个副本,对它的子字符串[li;ri]排序(从第i个字符到第i个字符的子字符串,包括两个端点)。注意,每个操作只影响一个副本,并且每个副本只受一个操作的影响。
你的任务是计算t1, t2,…,tm中不同字符串的个数。注意,只有在操作后至少有一个副本保持不变时,才应该计算初始字符串s。
输入
第一行包含一个整数t(1≤t≤104)——测试用例的数量。
每个测试用例的第一行包含两个整数n和m(1≤n,m≤2⋅105)—s的长度和拷贝数。
第二行包含n个字符0和/或1 -字符串s。
然后是m行。其中第i个包含两个整数li和Ri(1≤li≤Ri≤n)——对第i次拷贝执行的操作的描述。
n除以所有测试用例的和不超过2·105。m除以所有测试用例的和不超过2·105。
输出
打印一个整数- t1,t2,…,tm中不同字符串的个数。
测试样例:
输入
3
6 5
101100
1 2
1 3
2 4
5 5
1 6
6 4
100111
2 2
1 4
1 3
1 2
1 1
0
1 1
输出
3
3
1
二、解题思路
首先进行时间复杂度的分析,因为外层的 t <= 1e4,内层的 m <= 2e5,m*t <= 2e9,所以对于每一组数据,时间复杂度最好为O(m),当数据量不取极限值的时候,时间复杂度为O(n*logn)的算法勉强能过。
在排序的时候,无论前面的序列是什么,在最后面的1的始终不变;无论后面的序列是什么,在最前面的0也始终不变。所以每个区间的排序,事实上只对l 到 r 上第一个 1 到最后一个 0 的这部分区间进行排序所以,每次区间的有效区间就是 l 到 r 上第一个 1 到最后一个 0 的区间,,
有效区间不同,即要进行排序的区间不同,那么排序后得到的序列也一定不同。
所以,我们确定当前这一段区间上的有效区间后,把这个区间的有效区间[l',r']存储起来就可以,因为同一段有效区间排序结果一定是相同的,因为set有自动取重功能,所以我们用 set存储有效区间的两端l和r。
对于有效区间,我们只需要遍历一遍字符串就可以求出每一位字符的前一个 0 和 下一个 1,从而简单的确认出哪一部分是影响排序的。
用pre数组记录前面0的位置,即pre[i]表示前面离s[i]最近的0的下标(可以为i);
用next数组记录前面0的位置,即next[i]表示后面离s[i]最近的1的下标(可以为i);
因为pre记录的是前面的位置,所以确定pre[i]的值时,要对数组s从前往后遍历。当s[i]=='0'时,pre[i]=i,意思是s[i]离前面最近的0就是它本身,当s[i]=='1'时,pre[i]=pre[i-1],意思是s[i]离前面最近的0的下标和s[i-1]离前面最近的0的下标相同,而如果s[i-1]=='1',那么pre[i-1]==pre[i-2];以此类推,当s[i]!='1'时,pre[i]的值一定是由前面的序列得到,总之,pre[i]的值一定是正确的。
将pre[0]定义为0的原因,因为确定pre[i]的值,是从前往后找的,而当i==1,并且s[i]=='1'时,此时s[i]前面没有'0',将它前面的0的位置定义为下标0,如果pre[i]==0,说明他前面全是1,这是一个无效区间(不用进行排序的区间(r<=l ));
将next[n+1]定义为0的原因,因为确定next[i]的值,是从前往后找的,而当i==n,并且s[i]=='0'时,此时s[i]后面没有'1',将它后面的1的位置定义为下标n+1,如果next[i]==n+1,说明他后面全是0,这是一个无效区间(不用进行排序的区间(r<=l ));
l=next[l],将当前的左边界改为l 到 r 上第一个 1 的位置,l 到 r 上第一个排序时要变的位置。
r=pre[r], 将当前的右边界改为r 到 l 上第一个 0 的位置,r 到 l 上第一个排序时要变的位置。
即将把左边界往右扩, 右边界往左扩。
下面提到的r,l是指有效区间的右边界,左边界;
当r <= l 时,说明这个有效区间不存在,也就是说这种情况不需要进行排序,这种情况要特殊处理,在set里面存入{0,0}这对数;所有的 r <= l 的情况,都存入{0,0}这对数,因为set有自动取重功能,不论存多少个{0,0},在set内部只显示一个{0,0};也就是说,不论 r 和 l 的值是多少,只要 r <= l ,有效区间就不存在,当前这个排序操作得到的数组就是原数组,无论多少个这样的操作、多少个这样的 l 和 r,得到的都是原数组,都是这一种类型的数组。
三、代码实现
#include<iostream>
#include<set>
using namespace std;
int main( )
{
int t; scanf( "%d" , &t );
while( t-- )
{
int n , m; scanf( "%d %d" , &n , &m );
char s[ n * 2 ]; scanf( "%s" , s + 1 );
int next[ n + 3 ] , pre[n+3] ; //标记后面的1的位置;标记前面的0的位置;
next[ n + 1 ] = n + 1;
pre[ 0 ] = 0;
for ( int i = 1 ; i <= n; i ++ )
if ( s[ i ] == '0' ) pre[ i ] = i;
else pre[i] = pre[ i - 1 ];
for ( int i = n ; i >= 0 ; i -- )
if ( s[ i ] == '1' ) next[ i ] = i;
else next[ i ] = next[ i + 1 ];
set< pair < int , int > >st;
while ( m-- )
{
int l,r;
scanf( "%d %d" , &l , &r );
l = next[l], r = pre[r];
if (l > r) l = r = 0;
st.insert( { l , r } );
}
printf( "%d" , st.size( ) );
puts("");
}
return 0;
}
四、总结
有些时候要注意时间复杂度和空间复杂度,注意内存是否超限,这题我本来的思路是将区间(l,r)部分截取,存储到smid;并且将区间(l,r)前面的部分截取,存储到sl;将区间(l,r)后面的部分截取,存储到sr;随后用<algorithm>头文件中的sort函数对smid进行排序,在将sl+smid(排序后的)+sr,得到一个序列s1,用map<string,int>m,将s1存入map,最后输出map的长度;
sort函数的时间复杂度为n*logn,所以每组数据的时间复杂度为O(m*n*logn)一定会超时,而且一直用map存储排序后的各个字符串,也导致空间超限。
想到一个思路时,还是要先分析时间复杂度在进行代码的实现。