牛客周赛59(A,B,C,D,E二维循环移位,F范德蒙德卷积)

比赛链接

官方讲解

很幸运参加了内测,不过牛客这消息推送天天发广告搞得我差点错过内测消息,差点进小黑屋,好在开赛前一天看到了。

这场不难,ABC都很签到,D是个大讨论,纯屎,E是需要对循环移位进行处理和转化,转化后很简单,如果看不懂我写的可以看官方的讲解,画图可能会比较好理解,F是数学,需要分开计算贡献,最后再来一步范德蒙德卷积优化,知识点不难就是比较冷门。


A TD

思路:

签到

code:

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

int a,b;

int main(){
	cin>>a>>b;
	cout<<1.0*a/b;
	return 0;
}

B 你好,这里是牛客竞赛

思路:

如果是以 https://www.nowcoder.com 或者 www.nowcoder.com 作为前缀开头的话,那就是牛客主站,输出 Nowcoder。如果是以 https://ac.nowcoder.com 或者 ac.nowcoder.com 作为前缀开头的话,那就是牛客竞赛,输出 Ac。剩下的就是其他网站。

找子串可以用 s t r i n g string string 自带的 f i n d find find 方法,它会返回第一个匹配的子串的下标,作为前缀开头的话就看 f i n d find find 的值是否为 0 0 0 即可,是 0 0 0 就说明一开始就匹配上了,也就是前缀。

code:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

int T;
string s;

int main(){
	cin>>T;
	while(T--){
		cin>>s;
		if(s.find("https://ac.nowcoder.com")==0 || s.find("ac.nowcoder.com")==0)
			cout<<"Ac"<<endl;
		else if(s.find("https://www.nowcoder.com")==0 || s.find("www.nowcoder.com")==0)
			cout<<"Nowcoder"<<endl;
		else cout<<"No"<<endl;
	}
	return 0;
}

C 逆序数

思路:

一个序列里对任意的一对数只有正序和逆序两种关系,不是逆序就是正序。在一个序列里任取两个数的取法有 C n 2 = n ∗ ( n − 1 ) 2 C_n^2=\dfrac{n*(n-1)}2 Cn2=2n(n1) 种可能,也就是一共有这么多个关系,除了 k k k 个逆序,剩下的就有 n ∗ ( n − 1 ) 2 − k \dfrac{n*(n-1)}2-k 2n(n1)k 个正序。

而翻转之后,正序就会变成逆序,逆序变成正序,不管你这个集合是怎么凑的,翻转之前是 k k k 个逆序, n ∗ ( n − 1 ) 2 − k \dfrac{n*(n-1)}2-k 2n(n1)k 个正序,翻转之后就是 n ∗ ( n − 1 ) 2 − k \dfrac{n*(n-1)}2-k 2n(n1)k 个逆序, k k k 个正序。

code:

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

long long n,k;

int main(){
	cin>>n>>k;
	cout<<n*(n-1)/2-k<<endl;
	return 0;
}

D 构造mex

思路:

我嘞个大讨论啊,屎题。我已经能预想到那好看的通过率了。

我讨论的方法如下:

  1. k = 0 k=0 k=0 时,就尽量让每个数都是 1 1 1,最后一个数放剩余的还没用掉的值。
    1. s < n s<n s<n 时,这时甚至做不到让每个位置都是 1 1 1,一定会有位置为 0 0 0,因此无解
    2. s ≥ n s\ge n sn 时,就可以用上面的思路来凑。
  2. k ≠ 0 k\not=0 k=0 时,先凑出 0 , 1 , 2 , … , k − 1 0,1,2,\dots,k-1 0,1,2,,k1,这样 m e x mex mex 就等于 k k k 了,然后再把剩余的值放在后面,剩余位置就都放 0 0 0。前面 0 , 1 , 2 , … , k − 1 0,1,2,\dots,k-1 0,1,2,,k1 一共 k k k 个数,总和为 t o t = k ∗ ( k − 1 ) 2 tot=\dfrac{k*(k-1)}2 tot=2k(k1)
    1. 有可能 s < t o t s<tot s<tot,这时连前面的 k k k 个数都凑不出来,因此无解。
    2. 有可能 s = t o t s=tot s=tot,这时正好没有剩余的数,因此凑出前 k k k 个数后后面都可以补 0 0 0
      1. 当位置个数 n < k n<k n<k 时,位置不够,因此无解。
      2. n ≥ k n\ge k nk 时,位置够,用上面的方法来凑。
    3. 有可能 s − t o t = k s-tot=k stot=k,这时不能直接把剩余的值 s − t o t s-tot stot 放在后面,必须先拆出一个单独的 1 1 1 ,然后再把 s − t o t − 1 s-tot-1 stot1 放下,后面补 0 0 0
      1. 有可能 s − t o t = k = 1 s-tot=k=1 stot=k=1,这时做不到先拆出一个单独的 1 1 1 ,然后再把 s − t o t − 1 s-tot-1 stot1 放下,因为也会冲突,这时无解。
      2. 有可能位置个数不够,即 n < k + 2 n<k+2 n<k+2,这时无解
      3. 剩余情况,按上面的方法来凑。
    4. 按上面的方法来凑需要至少 k + 1 k+1 k+1 个位置,如果位置不够则无解,否则按上面的方法来凑。

code:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;

ll T,s,n,k;

int main(){
	cin>>T;
	while(T--){
		cin>>s>>n>>k;
		if(k==0){
			if(s<n)cout<<"NO"<<endl;
			else {
				cout<<"YES"<<endl;
				cout<<s-n+1<<" ";
				for(int i=1;i<=n-1;i++)cout<<1<<" ";
				cout<<endl;
			}
		}
		else {
			ll tot=k*(k-1)/2;
			if(s<tot)cout<<"NO"<<endl;
			else {
				if(k==1 && s-tot==1)cout<<"NO"<<endl;
				else {
					if(s-tot==0){
						if(n<k)cout<<"NO"<<endl;
						else {
							cout<<"YES"<<endl;
							for(int i=0;i<k;i++)cout<<i<<" ";
							for(int i=k+1;i<=n;i++)cout<<0<<" ";
							cout<<endl;
						}
					}
					else if(s-tot==k){
						if(n<k+2)cout<<"NO"<<endl;
						else {
							cout<<"YES"<<endl;
							for(int i=0;i<k;i++)cout<<i<<" ";
							cout<<1<<" "<<s-tot-1<<" ";
							for(int i=k+3;i<=n;i++)cout<<0<<" ";
							cout<<endl;
						}
					}
					else {
						if(n<k+1)cout<<"NO"<<endl;
						else {
							cout<<"YES"<<endl;
							for(int i=0;i<k;i++)cout<<i<<" ";
							cout<<s-tot<<" ";
							for(int i=k+2;i<=n;i++)cout<<0<<" ";
							cout<<endl;
						}
					}
				}
			}
		}
	}
	return 0;
}

E 小红的X型矩阵

思路:

对一个正方形,我们可以统计出它两个对角线上有多少个 1 1 1,对角线上的位置总个数减去对角线上 1 1 1 的个数就是对角线上 0 0 0 的个数,也就是需要用操作 1 1 1 0 0 0 翻转成 1 1 1 的次数。然后再统计出一共有多少个 1 1 1,减去对角线上 1 1 1 的个数就是对角线以外其他位置上 1 1 1 的个数,也就是需要需要用操作 1 1 1 1 1 1 翻转成 0 0 0 的次数。这个快速统计对角线上 1 1 1 的个数可以用斜着的前缀和来做。

这个循环移动我们可以看作是移动这两个对角线,但是我们对角线一旦循环移动了就会断成两段,就很难计数了,而且对于 n n n 为奇数的情况,两个对角线还会有交点,我们交点只能算一次,但是用前缀和来快速计数就会算两次,很麻烦。

这时我们可以参考数组的循环移位,把数组复制一遍到后面。我们可以把这个正方形往 x , y x,y xy 轴各复制一遍,这样从 ( i , j ) (i,j) (i,j) ( i + n − 1 , j + n − 1 ) (i+n-1,j+n-1) (i+n1,j+n1) 的这个矩形就相当于原矩形向左循环移位 i − 1 i-1 i1 次,向上循环移位 j − 1 j-1 j1 次。

我们枚举矩形的左上角坐标 ( i , j ) (i,j) (i,j),这样对每一个正方形都对应一个循环移位后的正方形,统计所有正方形的答案,然后取最小值即可。

注意 n n n 为奇数的时候,中间会有一个交点,前缀和对两个对角线计数会重复算一次,需要减掉。

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1005;

int n,a[maxn<<1][maxn<<1],s1[maxn<<1][maxn<<1],s2[maxn<<1][maxn<<1],tot;

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){
			cin>>a[i][j];
			tot+=a[i][j];
			a[i+n][j]=a[i][j+n]=a[i+n][j+n]=a[i][j];
		}
	
	for(int i=1;i<=2*n;i++)
		for(int j=1;j<=2*n;j++)
			s1[i][j]=s1[i-1][j-1]+a[i][j];
	for(int i=1;i<=2*n;i++)
		for(int j=1;j<=2*n;j++)
			s2[i][j]=s2[i-1][j+1]+a[i][j];
	
	int ans=1e9;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){//枚举左上角坐标 
			int t=0,t1=s1[i+n-1][j+n-1]-s1[i-1][j-1],t2=s2[i+n-1][j]-s2[i-1][j+n];
			t=t1+t2;
			if(n&1){
				t-=a[i+n/2][j+n/2];
				ans=min(ans,(2*n-1)-t+tot-t);
			}
			else {
				ans=min(ans,2*n-t+tot-t);
			}
		}
	cout<<ans<<endl;
	return 0;	
}

F 小红的数组回文值

思路:

内测时写了一个很神秘的暴力,结果过了,然后被加强数据卡死了。。。这个暴力其实本质上和正解差不多,就差一个范德蒙德卷积优化kuso。

这个题是求所有子序列的回文值,但是我们显然不可能枚举出所有的子序列,所以我们不妨转化一下视角,看每一对数的贡献。

当一对数不同,并且在某一个回文串中正好对应,那么它就会产生一次贡献,假设第 i i i 个位置和第 j j j 个位置的数在某一个回文串中对应,并且 a i ≠ a j a_i\not=a_j ai=aj。怎么算出所有满足这两个数对应的回文串呢?

我们可以在前 i − 1 i-1 i1 个位置和后 n − j n-j nj 个位置同时选出 k k k 个数,在中间的 j − i − 1 j-i-1 ji1 个数中选若干个数,这样就凑出一个 a i a_i ai a j a_j aj 对应的回文串了。

假设前后各有 x , y x,y x,y 个数,也就是 x = i − 1 , y = n − j x=i-1,y=n-j x=i1,y=nj,那么选取的方案数就是 ∑ k = 0 m i n { x , y } C x k ∗ C y k ∗ 2 n − x − y − 2 \sum_{k=0}^{min\{x,y\}} C_x^k*C_y^k*2^{n-x-y-2} k=0min{x,y}CxkCyk2nxy2

因为 n = 2000 n=2000 n=2000,所以我们可以枚举 i , j i,j i,j,算出对应的 x , y x,y x,y,然后再枚举 k k k。这样就是一个 O ( n 3 ) O(n^3) O(n3) 的算法,会 T T T,但是如果我们如果能把枚举 k k k 的过程优化掉,就可以压成 O ( n 2 ) O(n^2) O(n2) 的了。

还真能, 2 n − x − y − 2 2^{n-x-y-2} 2nxy2 k k k 无关,可以提出来,前面的这个 ∑ k = 0 m i n { x , y } C x k ∗ C y k \sum_{k=0}^{min\{x,y\}} C_x^k*C_y^k k=0min{x,y}CxkCyk,我们不妨设 x < y x<y x<y(不然就交换 x , y x,y x,y),则 = ∑ k = 0 x C x k ∗ C y k = ∑ k = 0 x C x x − k ∗ C y k =\sum_{k=0}^{x} C_x^k*C_y^k=\sum_{k=0}^{x} C_x^{x-k}*C_y^k =k=0xCxkCyk=k=0xCxxkCyk 这一步可以通过范德蒙德卷积优化,得到 = C x + y x =C_{x+y}^{x} =Cx+yx。范德蒙德卷积讲解可以看 OI wiki

code:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int maxn=2e3+5;
typedef long long ll;
const ll mod=1e9+7;

int n,a[maxn];
ll qpow(ll a,ll b){
	ll ans=1,base=a%mod;
	b%=mod;
	while(b){
		if(b&1)ans=ans*base%mod;
		base=base*base%mod;
		b>>=1;
	}
	return ans;
}
ll inv(ll x){return qpow(x,mod-2);}

ll fac[maxn],ifac[maxn];
ll C(ll x,ll y){//C_x^y
	if(x<y)return 0;
	return fac[x]*ifac[y]%mod*ifac[x-y]%mod;
}

ll calc(ll x,ll y){
	if(x>y)swap(x,y);
	return qpow(2,n-x-y-2)*C(x+y,x)%mod;
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	
	fac[0]=1;
	for(int i=1;i<=2000;i++)fac[i]=fac[i-1]*i%mod;
	ifac[2000]=inv(fac[2000]);
	for(int i=2000;i>=1;i--)ifac[i-1]=ifac[i]*i%mod;
	
	ll ans=0;
	for(int i=1;i<=n;i++)
		for(int j=i+1;j<=n;j++)
			if(a[i]!=a[j])
				ans=(ans+calc(i-1,n-j));
	cout<<(ans%mod+mod)%mod<<endl;
	return 0;
}

内测时成功卡AC的 O ( n 3 ) O(n^3) O(n3) 暴力代码(虽然最后还是被卡掉了):

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int maxn=2e3+5;
typedef long long ll;
const ll mod=1e9+7;

int n,a[maxn],b[maxn];
ll qpow(ll a,ll b){
	ll ans=1,base=a%mod;
	b%=mod;
	while(b){
		if(b&1)ans=ans*base%mod;
		base=base*base%mod;
		b>>=1;
	}
	return ans;
}
ll inv(ll x){return qpow(x,mod-2);}

ll fac[maxn],ifac[maxn];
ll C(ll x,ll y){//C_x^y
	if(x<y)return 0;
	return fac[x]*ifac[y]%mod*ifac[x-y]%mod;
}

vector<int> c[maxn];

ll calc(ll x,ll y){
	ll ans=0;
	for(int i=0;i<=min(x,y);i++)
		ans=(ans+C(x,i)*C(y,i))%mod;
	return qpow(2,n-x-y-2)*ans%mod;
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	memcpy(b,a,sizeof(b));
	sort(b+1,b+n+1);
	int tot=unique(b+1,b+n+1)-b-1;
	auto find=[&](int x)->int{return lower_bound(b+1,b+n+1,x)-b;};
	for(int i=1;i<=n;i++)a[i]=find(a[i]);
	for(int i=1;i<=n;i++)c[a[i]].push_back(i);
	
	fac[0]=1;
	for(int i=1;i<=2000;i++)fac[i]=fac[i-1]*i%mod;
	ifac[2000]=inv(fac[2000]);
	for(int i=2000;i>=1;i--)ifac[i-1]=ifac[i]*i%mod;
	
	ll ans=0;
	for(int i=1;i<=n;i++){
		ans=(ans+C(n,i)*(i/2)%mod)%mod;
	}
	for(int i=1;i<=n;i++){
		for(int l=0;l<c[i].size();l++)
			for(int r=l+1;r<c[i].size();r++){
				ans=(ans-calc(c[i][l]-1,n-c[i][r]))%mod;
			}
	}
	cout<<(ans%mod+mod)%mod<<endl;
	return 0;
}
  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值