Codeforces #562 Div.1解题报告

A.二分+贪心
http://codeforces.com/contest/1168/problem/A
题目意思是,给你一个n元数组( a i &lt; m a_i&lt;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 &lt; m {a_k}&#x27;∈[a_k,a_k+x] \quad if \quad a_k+x&lt;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}&#x27;∈[a_k,m-1]\quad or\quad [0,a_k+x-m] elseak[ak,m1]or[0,ak+xm]
且:
a k &gt; = a k − 1 i f k &gt; 0 a_k &gt;= a_{k-1} \quad if \quad k&gt;0 ak>=ak1ifk>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 &lt; &lt; j ) = = 1 y \quad and \quad (1&lt;&lt;j)\quad == \quad 1 yand(1<<j)==1如果有 n x t [ i ] [ j ] &lt; = y nxt[i][j]\quad &lt;=\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种的边。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值