A.二分+贪心
http://codeforces.com/contest/1168/problem/A
题目意思是,给你一个n元数组(
a
i
<
m
a_i<m
ai<m),每次操作,你可以任选一些元素,使其+1 再对m取模。问至少多少次操作可以使得这个数列单调不减。
先看数据范围,1e5数量级,那么上限是nlogn的算法。不难发现答案满足单调性质,所以我们考虑二分答案,希望设计出O(n)内的check算法。这样的复杂度至少要从左往右扫一遍每个元素,在扫描的过程中,如果左边的元素更小,对于右边的元素来说是严格更优的,这样就满足贪心的性质。所以我们考虑使用贪心法来解决问题。
现在我们假设x次操作可以实现目标,因为有n个元素,那么至少需要从左往右扫一遍。当我们考虑第k个元素
a
k
a_k
ak,事实上
a
k
a_k
ak满足如下性质:
a
k
′
∈
[
a
k
,
a
k
+
x
]
i
f
a
k
+
x
<
m
{a_k}'∈[a_k,a_k+x] \quad if \quad a_k+x<m
ak′∈[ak,ak+x]ifak+x<m
e
l
s
e
a
k
′
∈
[
a
k
,
m
−
1
]
o
r
[
0
,
a
k
+
x
−
m
]
else \quad {a_k}'∈[a_k,m-1]\quad or\quad [0,a_k+x-m]
elseak′∈[ak,m−1]or[0,ak+x−m]
且:
a
k
>
=
a
k
−
1
i
f
k
>
0
a_k >= a_{k-1} \quad if \quad k>0
ak>=ak−1ifk>0
那么我们可以先利用第一个公式对每个元素用O(1)时间求出 a k a_k ak的可行域,然后再判断在可行域中最小可以取什么值,这同样可以在O(1)时间内解决。所以最终复杂度就是O(nlogn),满足要求。
在实际编码是,先二分答案,每次二分从左往右扫一边,保存一个pre值表示之前一个数的大小,然后对于每个数求出其可行域,然后在可行域中找到最小的>=pre的值。
代码如下:
/*a[] 表示原数组*/
/*t[]表示如何保存可行域,$a_0,a_1;a_2,a_3$分别保存两个闭区间的端点*/
#include<bits/stdc++.h>
using namespace std;
template <class T>
T read(T &x)
{
x=0;T f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x*=f;return x;
}
int n,m;
int a[300005],pre,t[4];
int l,r;
bool check(int x){
pre=0;
for(int i=0;i<n;++i){
t[0]=t[1]=-1;
if(a[i]+x>=m){
t[0]=0;
t[1]=a[i]+x-m;
}
t[2]=a[i];
t[3]=min(a[i]+x,m-1);
if(t[0]<=pre&&pre<=t[1])pre=pre;
else if(t[1]+1<=pre&&pre<=t[2]-1)pre=t[2];
else if(t[2]<=pre&&pre<=t[3])pre=pre;
else return 0;
}
return 1;
}
int main(){
read(n),read(m);
for(int i=0;i<n;++i)read(a[i]);
l=-1,r=m;
while(r>l+1){
int m=(l+r)>>1;
if(check(m))r=m;
else l=m;
}
cout<<r<<endl;
return 0;
}
B.忽悠人题+打表
http://codeforces.com/contest/1168/problem/B
给你一个01串,问你有多少个子串,满足里面至少有一组3个0或者1使得他们等间距。
这题乍一看,感觉像是个数据结构题。但是再一看,01串的结构非常简单,而这个triple也非常简单,爆搜一下发现,当串长度大于等于9的时候,都是满足条件的。所以接下来就只要穷举长度为1~8的串看是否满足条件即可。对于每个串,用两重循环确定三个点爆搜即可,一个串不多于64次操作四舍五入等于0,串的个数不多于8n个最后复杂度512n。
然后为了写的方便一点,我拿爆搜的代码打了表,最后复杂度玄学的下降了很多,因为不含triple的串其实非常少。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define INI(x) memset(x,0,sizeof(x))
#define MAX(x,y) (((x)>(y))?(x):(y))
#define MIN(x,y) (((x)<(y))?(x):(y))
#define ABS(x) (((x)>0)?(x):-(x))
const double EPS=1e-8;
const LL MOD=1e9+7;
const LL inf=0x3f3f3f3f;
const LL INF=0x3f3f3f3f3f3f3f3f;
template <class T>
T read(T &x)
{
x=0;T f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x*=f;return x;
}
string s;
LL n;
LL ans=0;
LL p[9]={0,2,4,6,10,14,20,16,6};
string c[10][20]={{"0"},
{"0","1"},
{"00","01","10","11"},
{"001","010","011","100","101","110"},
{"0010","0011","0100","0101","0110","1001","1010","1011","1100","1101"},
{"00100","00101","00110","01001","01011","01100","01101","10010","10011","10100","10110","11001","11010","11011"},
{"001001","001011","001100","001101","010010","010011","010110","011001","011010","011011","100100","100101","100110","101001","101100","101101","110010","110011","110100","110110"},
{"0010011","0011001","0011010","0011011","0100101","0101100","0101101","0110011","1001100","1010010","1010011","1011010","1100100","1100101","1100110","1101100"},
{"00110011","01011010","01100110","10011001","10100101","11001100"}};
int main(){
cin>>s;
n=s.size();
s="0"+s;
for(LL i=1;i<=n;++i){
LL len;
for(len=3;i+len-1<=n&&len<=8;++len){
string tmp=s.substr(i,len);
LL flag=0;
for(LL j=0;j<p[len];++j){
if(tmp==c[len][j])flag=1;
}
if(!flag)break;
}
ans+=max(n-len-i+2,0ll);
}
cout<<ans<<endl;
return 0;
}
C.dp预处理nxt数组
http://codeforces.com/contest/1168/problem/C
给你一串数,其中如果两个数的element-wise and不为0,则可以从左边的数走到右边的数上,给你q组询问,能都从左边的数走到右边的数上。
这个题看到第一反应是图,然后转化为可达性问题。但是建图需要 n 2 n^2 n2(穷举每个数) ∗ l o g 2 k * log_2k ∗log2k(对每个数做逐位比较)。不说判断可达性的复杂度,光这个就已经爆了。那么如何从这种朴素想法去获得一个更低复杂度的算法呢? l o g 2 k log_2k log2k是不能省的,因为你肯定要对k种不同的连接分别讨论,那么我们只能对 n 2 n^2 n2下手了。
这种跳来跳去的模型,很类似于1对多的子序列包含问题。那么其中的nxt数组是否能在这里使用呢?
经过某些玄学的思考,可以构造这样的一个数组:
nxt[i][j]表示从i出发向右走,能够走到的最近的在 2 j 2^j 2j位上为1的数。考虑x到y的可达性,因为y必定是通过某个j抵达的,而所有含 2 j 2^j 2j项的数又是左可达右的。所以只要存在这样一个 j j j满足: y a n d ( 1 < < j ) = = 1 y \quad and \quad (1<<j)\quad == \quad 1 yand(1<<j)==1如果有 n x t [ i ] [ j ] < = y nxt[i][j]\quad <=\quad y nxt[i][j]<=y那么x必定可达y。
接下来只要预处理nxt数组即可。这个相对比较简单,类似于1对多的子序列包含问题,从右往左处理,保存一个lst数组,lst[j]表示在当前位置右边最近的含有 2 j 2^j 2j项的元素的下标。
那么如果某个位置 a i a_i ai含有 2 j 2^j 2j项,那么 n x t [ i ] [ j ] = i nxt[i][j]=i nxt[i][j]=i,否则穷举k,若 a i a_i ai含有 2 k 2_k 2k,那么考虑所有可以通过k抵达的位置 n x t [ i ] [ j ] = m i n ( n x t [ i ] [ j ] , n x t [ l s t [ k ] ] [ j ] ) nxt[i][j]=min(nxt[i][j],nxt[lst[k]][j]) nxt[i][j]=min(nxt[i][j],nxt[lst[k]][j])
给出代码:
#include<bits/stdc++.h>
using namespace std;
template <class T>
T read(T &x)
{
x=0;T f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x*=f;return x;
}
int nxt[300005][20];
int a[300005],lst[20];
int n,q;
int main(){
read(n),read(q);
for(int i=1;i<=n;++i)read(a[i]);
for(int j=0;j<20;++j)lst[j]=n+1,nxt[n+1][j]=n+1;
for(int i=n;i;--i){
for(int j=0;j<20;++j){
nxt[i][j]=n+1;
if(a[i]&(1<<j))nxt[i][j]=i;//说明a[i]本身就有2^j项,所以最近的就是自己
else{//如果自己没有2^j项,那么只能去找能不能间接接触到2^j项
for(int k=0;k<20;++k){//穷举自己所有的2^k项
if(a[i]&(1<<k))nxt[i][j]=min(nxt[i][j],nxt[lst[k]][j]);
/*找到最近的可达的含2^k项的位置(lst[k]),那么他能达到2^j为位的就是自己能达到的*/
}
}
}
for(int j=0;j<20;++j){
if(a[i]&(1<<j))lst[j]=i;
}
}
for(int i=1,x,y,m;i<=q;++i){
read(x),read(y);
m=n+1;
for(int j=0;j<20;++j){
if(a[y]&(1<<j))m=min(m,nxt[x][j]);
}
printf("%s\n",m>y?"Fou":"Shi");
}
return 0;
}
事实上,这个问题可以扩展为m种不同的边,本质上是一样的。因为按位分解就是分成 l o g 2 k log_2k log2k种的边。