CF Round #521 (Div. 3)

ACM题集:https://blog.csdn.net/weixin_39778570/article/details/83187443
题目链接:http://codeforces.com/contest/1077
官方题解:https://codeforces.com/blog/entry/63274

A题

简单的奇偶问题

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll t,a,b,k;
int main(){
	scanf("%I64d",&t);
	while(t--){
		scanf("%I64d%I64d%I64d",&a,&b,&k);
		ll ans =0;
		if(k&1){
			ans += a*(k/2+1)-b*(k/2);
		}else{
			ans += (a-b)*(k/2);
		}	
		cout<<ans<<endl; 
	}
	return 0;
}

B题

求最少关掉几栈灯等使得大家互不打扰,开关问题

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; i++)
using namespace std;
int n,a[105];
int main(){
	cin>>n;
	fo(i,1,n)cin>>a[i];
	int ans = 0;
	fo(i,3,n){
		if(a[i-2]==1&&a[i-1]==0&&a[i]==1){
			ans++;
			a[i]=0;
		}
	}
	cout<<ans;
}

C题

题意:删掉序列中的一个数,使得这个数列成为Good array(有一个数等于其余的数的和)
解法:删掉一个数变成good array
只有两种删法,排序后,删掉最后一个数,然后查看前n-2个数和是否等于a[n-1]
删掉中间的某个数,查看剩余数的和是否等于最后一个数
删掉的数为sum[n-1] - a[n], 计算该数有多少个就行(二分一下或者枚举)

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; i++)
using namespace std;
int read()
{
	int x=0,f=1;char c=getchar();
	while (c<'0'||c>'9') {if (c=='-') f=-1;c=getchar();}
	while (c>='0'&&c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}
struct node{
	int val,idx;
	bool operator < (const node &a)const{
		return val<a.val;
	}
}a[200005];
int n;
ll sum[200005]; // 千万注意 int 1e10就gg了 
vector<int> ver;
int my_upper_bound(int x){
	int L=1,R=n-1,mid,ans=n; // ans设置比上界大一 
	while(L<=R){
		mid=(L+R)>>1;
		if(a[mid].val>x){
			ans = mid;
			R = mid-1;
		}else{
			L = mid+1;
		}
	}
	return ans;
}
int my_lower_bound(int x){
	int L=1,R=n-1,mid,ans=n;// ans设置比上界大一 
	while(L<=R){
		mid=(L+R)>>1;
		if(a[mid].val>=x){
			ans = mid;
			R = mid-1;
		}else{
			L = mid+1;
		}
	}
	return ans;
}
void solve(){
	fo(i,1,n){
		sum[i] = sum[i-1]+a[i].val;
	}
	// 删掉非最后一个数,使得和等于最后一个数 
	ll t = sum[n-1] - a[n].val;
	if(t>0&&t<=a[n].val){  // 这个t一定要注意!!!要在查找范围内 
		int t2 = my_upper_bound(t);
		int t1 = my_lower_bound(t);
		fo(i,t1,t2-1){
			ver.push_back(a[i].idx); 
		}
//		fo(i,1,n-1)if(a[i].val==t)ver.push_back(a[i].idx); 
	}
	if(a[n-1].val==sum[n-2])ver.push_back(a[n].idx); // 删掉最后一个数 
	printf("%d\n",ver.size());
	for(int i=0; i<ver.size(); i++){
		printf("%d%c",ver[i],i==ver.size()-1?'\n':' ');
	}
}
int main(){
	scanf("%d",&n);
	fo(i,1,n){
	//	scanf("%d",&a[i].val);
		a[i].val = read(); 
		a[i].idx=i;
	}
	sort(a+1,a+1+n);
	if(n>2)solve();
	else puts("0");
	return 0;
}

C题别人做法

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; ++i)
using namespace std;
int n,a[200005];
map<int,int> mp; // 真的变慢 
vector<int> ver;
ll sum;
int main(){
	scanf("%d",&n);
	fo(i,1,n)scanf("%d",&a[i]),sum+=a[i],mp[a[i]]++;
	ll t;
	fo(i,1,n){
		t = sum - a[i];// 删掉一个数 
		if(t&1)continue;
		t>>=1; // 一半 
		if(t>0&&t<=1000000){ // 在合法范围内
		//	if((t!=a[i]&&mp[t]>=1) || (t==a[i]&&mp[t]>=2)){
			if(t!=a[i]&&mp[t]>=1 || t==a[i]&&mp[t]>=2){ // 找一个数等于一半的,注意可能刚好一半等于删掉的那个数(则至少需要两个) 
				ver.push_back(i);
			} 
		}
	}
	printf("%d\n",ver.size());
	for(int i:ver)printf("%d ",i);
	return 0;
}

D题

题意:n个数找k个数,要求这k个数出现的次数最少为times求times的最大值
解法:二分答案,即二分cut time 找到最大的cut time 更新答案

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; i++)
using namespace std;
struct node{
	int val,idx;
	// 按出现次数排序 
	bool operator < (const node &a)const{
		if(a.idx!=idx)return idx<a.idx;
		else{
			return val>a.val; // 出现次数相同按值排序 
		}
	}
}a[200005];
int n,k,mx=-1;
map<int,int> mp;
vector<int> ans;
bool ok(int mid){ // 重复次数 
	// 计算重复段(可剪次数)大于等于mid有几个数(包含重复数字)
	// 例如: mid=2。  1,1,1,1,1,1  有3个1满足 
	int t = 0,last = -1,loo=1;//重复次数 
	for(int i=n; i>=1;){
		if(a[i].val==last)loo++; // 和上一个重复了
		else loo=1,last=-1; // 新的数出现 
		if(a[i].idx>=mid*loo){
			last = a[i].val;
			t++;
			i-=mid;
		}else i--;
	}
	return t>=k; // 是否有大于等于k个字节 
}
// 获取答案,和ok函数差不多 
void get(int mid){
	int t = 0,last = -1,loo=1;
	for(int i=n; i>=1;){
		if(a[i].val==last)loo++;
		else loo=1,last=-1;
		if(a[i].idx>=mid*loo){
			last = a[i].val;
			ans.push_back(a[i].val);
			t++;
			i-=mid;
		}else i--;
		if(t==k)break;
	}
}
void solve(){
	int L=1,R=mx,mid,ANS=1;
	// 二分最大重复段 
	while(L<=R){
		mid = (L+R)>>1;
		if(ok(mid)){
			L = mid+1;	
			ANS = mid;		
		}else R = mid-1; 
	} 
	get(ANS);
	int t = ans.size();
	for(int i=0;i<t; i++){
		printf("%d%c",ans[i],i==t-1?'\n':' ');
	}
}
int main(){
	cin>>n>>k;
	fo(i,1,n){
		scanf("%d",&a[i].val);
		mp[a[i].val]++;
	}
	fo(i,1,n){
		a[i].idx = mp[a[i].val]; // 出现次数 
		mx = max(mx,a[i].idx);
	}
	sort(a+1,a+1+n); // 先对次数排序
//	for(int i=1;i<=n; i++){
//		printf("%d%c",a[i].val,i==n?'\n':' ');
//	}
	solve();
}

D题其他解法

#include<bits/stdc++.h>
using namespace std;
int b[300000];
int a[300000];
int main()
{
	int n,k;
	cin>>n>>k;
	for(int i=0;i<n;i++){
		int x;
		cin>>x;
		b[x]++;
	}
	priority_queue<pair<int,int> >pq,res;
	for(int i=0;i<300000;i++)
	{
		if(b[i])pq.push({b[i],i});
	}
	for(int i=0;i<k;i++)
	{
		cout<<pq.top().second<<" "; //大根堆,堆顶必是答案
		pair<int,int>h=pq.top();
		pq.pop();
		a[h.second]++;
		h.first=b[h.second]/(a[h.second]+1); // 要出现 a[h.second]+1次的话每个数只能被cutb[h.second]/(a[h.second]+1)次 
		pq.push(h);// 剩下的次数,进入排序了 
	}
}

E题

题意:一堆数中找到和最大的倍增序列
解法:我们可以很容易知道,这题的数值是没有作用的,因为每个数只能在在以个contenes中使用,所以我们只用考虑每个数出现的次数,得到一个序列
我一开始的做法
按次数排序 ,然后对值去重,
枚举每个每个数做为起点选择话题,再枚举1~这个数的出现次数,做为倍增序列 的第一个起点数
然后算。。。。然后T了
上面的思想是从最小的开始枚举起
我们可以做一步优化,从最大的递推算出起点 val[i] = min(val[i+1]/2, ver[i]) ,最后面的val设置为最大
也就是说,我们每一步都可以知道倍增序列的起点,和长度,
长度为i,起点为val[i]的倍增序列的和为
a[i] * ((1<<i)-1)

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; i++)
using namespace std;
const int maxn = 2e5+5;
int n;
map<int,int>mp;
vector<int> ver;
int val[maxn];
int calc(){
	n = ver.size();
	int ans = val[n-1] = ver[n-1]; // 取最大次数做为第一次比赛,仅此一次比赛 
	for(int i=n-2; i>=0; i--){
		// val[i]作为第一次比赛 
		val[i] = min(val[i+1]/2, ver[i]); // 保证val[i] 与后面的数可以形成 val[i],2*val[i],4*val[i]...这样的序列 
		if(val[i]==0) break; // 不会再更新答案了 
		// val[i],val[i+1]...val[n-1]共 n-i个数, 1+2+4+8... = 2^(n-i)-1 
		int t =  val[i] * ((1<<(n-i))-1);
		ans = max(ans,t);
	} 
	return ans;
}
int main(){
	cin>>n;int a;
	// 值去重,次数排序 
	fo(i,1,n){
		scanf("%d",&a);
		mp[a]++; // 去重 
	}	
	for(auto it : mp) ver.push_back(it.second);
	sort(ver.begin(), ver.end()); 
	cout<<calc();
}

F1题

题意:在n个数里选x个数,并且这n个数中,每k个数至少要有一个数被选中,求选中的x个数和最大
解法:DP! 连续不超过k的区间转移到下一个点
d p [ i ] [ j ] dp[i][j] dp[i][j]表示选了i个选到了第j个的最大价值
d p [ i ] [ j ] dp[i][j] dp[i][j] d p [ i − 1 ] dp[i-1] dp[i1][前面的k区间] 转移过来
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ h ] ) , h = [ m a x ( 0 , j − k ) , j − 1 ] dp[i][j] = max(dp[i-1][h]),h=[max(0,j-k),j-1] dp[i][j]=max(dp[i1][h]),h=[max(0,jk),j1]

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register ll i=j; i<=n; ++i)
using namespace std;

ll n,k,x,a[205],dp[205][205];
void solve(){
	memset(dp,-1,sizeof(dp));
	dp[0][0]=0;
	fo(i,1,x){ // 选了i个 
		fo(j,i,n){ // 选到第j个 
			for(ll h=max(0ll,j-k); h<=j-1; h++){ // 可转移的区间h, dp[i-1][h]+a[j]->dp[i][j] 
				if(dp[i-1][h]!=-1) dp[i][j] = max(dp[i][j], dp[i-1][h]+a[j]);
			}
		}
	}
	ll mx = -1;
	// 最后[n-k+1,n]之间至少要有一个数被挑选到才能是一个合格的序列 
	for(int i=n-k+1; i<=n; i++)mx = max(mx, dp[x][i]);
	cout<<mx;
}
int main(){
	scanf("%lld%lld%lld",&n,&k,&x);
	fo(i,1,n)scanf("%lld",&a[i]);
	solve();
	return 0;
}

F2题

题意:上面的F1题数据量加大,需要O(nx)时间复杂度的算法
解法:主要优化这一步: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ h ] ) , h = [ m a x ( 0 , j − k ) , j − 1 ] dp[i][j] = max(dp[i-1][h]),h=[max(0,j-k),j-1] dp[i][j]=max(dp[i1][h]),h=[max(0,jk),j1]
使其能在O(1)时间内算出。距离做法使用单调递减列队优化。
每次取队头就行
f [ i ] [ i ] f[i][i] f[i][i] f [ i − 1 ] [ i − 1 ] f[i-1][i-1] f[i1][i1]转移过来最优,因为同样是选了 i − 1 i-1 i1个, j = i − 1 j=i-1 j=i1的时候选择最多也最优
f [ i ] [ j ] f[i][j] f[i][j] f [ i − 1 ] [ i − 1 ] 到 f [ i − 1 ] [ j − 1 ] f[i-1][i-1] 到 f[i-1][j-1] f[i1][i1]f[i1][j1]转移过来, [ i − 1 , j − 1 ] [i-1,j-1] [i1,j1]在k区间合法范围内

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; ++i)
using namespace std;
const int maxn = 5005;
int n,k,x,a[maxn],q[maxn];
ll f[maxn][maxn];
void solve(){
	fo(i,1,n)f[0][i] = -1e15;
	int l,r;
	fo(i,1,x){
		// f[i][i] 从 f[i-1][i-1]转移过来最优 
		// f[i][j] 从 f[i-1][i-1] 到 f[i-1][j-1]转移过来,[i-1,j-1]在k区间合法范围内 
		q[l=r=1] = i-1;// 一个新的单调递增减列队,使用闭区间 
		fo(j,i,n){
			while(l<=r && q[l]<j-k) l++; // 删除无用位置,左区间超过k区间[j-k,j-1]范围
			if(l<=r)f[i][j] = f[i-1][q[l]] + a[j];
			while(l<=r && f[i-1][q[r]]<=f[i-1][j]) r--;// 删除无用的队尾 
			q[++r] = j; // 存入新的比较好的下标 
		}
	}
	ll mx = -1;
	// 可能的答案区间1 
	for(int i=n-k+1; i<=n; i++) mx = max(mx, f[x][i]);
	cout<<mx;
}
int main(){
	cin>>n>>k>>x;
	fo(i,1,n)scanf("%I64d",&a[i]);
	solve();
	return 0; 	
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值