第二十届西南科技大学ACM程序设计竞赛(同步赛)C E I K L M 题

题目链接

C 题

        对于求区间【l,r】的和可以利用前缀和的思想,转化成【1,r】-【1,l-1】的和,但由于该题的每个不同区间的单个值是分块分布的,所以在求和时我们先求区间包括的完整区间的和,然后再加上多余的部分的值。

        我们考虑插入新位置对原数轴的影响,可以发现新插入的位置只会影响新位置左端最靠近的位置与新位置右端最靠近的位置,所以我们考虑如何建树才可以对这样的区间进行修改。

        我们设上述的三个位置为l,mid,r,我们需要修改的区间为【l+1,mid-1】和【mid+1,r-1】。因为区间的值只和区间的端点值有关,所以我们考虑用区间的两个端点来表示这个区间,也就是说对于原区间【l,r】我们用(r-l-1)乘以左端点的权值,在乘以右端点的权值。那么对于新添的mid点,我们考虑直接将【l+1,mid-1】和【mid+1,r-1】的值进行修改就好了。

        那么接下来我们思考每插入一个值的时候如何对分开的区间进行修改。我们可以将两个区间用他们的右端点表示,每次插入他们的右端点,然后修改该点的最靠近左端点到当前右端点间的区间。

        由于我们还要求除去完整区间的和,还要求多余部分的和,我们用map来记录每个区间里每个点的权值,然后乘个数就可以了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int inf=0x3f3f3f3f;
const int N=3e5+10;
const int mod=1e9+7;
#define fi first
#define se second
#define int ll
int n,m,q,a[N],b[N];
int tree[N*4],cnt[N*4];
set<int> s;
map<pii,int> v;
void insert(int p,int l,int r,int x){
	if(l==r){
		cnt[p]++;
		auto pos=s.lower_bound(x);
		pos--;
		int preid=*pos;
		tree[p]=(x-preid-1)*b[x]*b[preid];
		return;
	}
	int mid=(l+r)/2;
	if(x<=mid) insert(p*2,l,mid,x);
	else insert(p*2+1,mid+1,r,x);
	tree[p]=tree[p*2]+tree[p*2+1];
}
int query(int p,int l,int r,int x,int y){
	if(x<=l&&y>=r){
		return tree[p];
	}
	int mid=(l+r)/2;
	int res=0;
	if(x<=mid) res+=query(p*2,l,mid,x,y);
	if(y>mid) res+=query(p*2+1,mid+1,r,x,y);
	return res;
}
void solve(){
	cin>>n>>m>>q;
	for(int i=1;i<=m;i++){
		cin>>a[i];
		s.insert(a[i]);
	}
	for(int i=1;i<=m;i++){
		int x;cin>>x;
		b[a[i]]=x;
		v[{a[i-1],a[i]}]=b[a[i-1]]*b[a[i]];
	}
	for(int i=2;i<=n;i++){
		insert(1,1,n,a[i]);
	}
	while(q--){
		int op,x,y;cin>>op>>x>>y;
		if(op==1){
			b[x]=y;
			s.insert(x);
			auto pos=s.lower_bound(x);
			pos--;
			int preid=*pos;
			v[{preid,x}]=y*b[preid];
			insert(1,1,n,x);
			pos=s.upper_bound(x);
			int nexid=*pos;
			v[{x,nexid}]=y*b[nexid];
			insert(1,1,n,nexid);
		}
		else{
			int res=0,prelid,nexlid,prerid,nexrid;
			if(b[x]){
				res-=query(1,1,n,1,x);
			}
			else{
				auto pos=s.lower_bound(x);
				nexlid=*pos;
				pos--;
				prelid=*pos;
				res-=query(1,1,n,1,prelid);
				res-=(x-prelid-1)*v[{prelid,nexlid}];
			}
			if(b[y]){
				res+=query(1,1,n,1,y);
			}
			else{
				auto pos=s.upper_bound(y);
				nexrid=*pos;
				pos--;
				prerid=*pos;
				res+=query(1,1,n,1,prerid);
				res+=(y-prerid)*v[{prerid,nexrid}];
			}
			cout<<res<<endl;
		}
	}
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int t=1;
	//cin>>t;
	while(t--){
		solve();
	}
	return 0;
}

E 题

        贪心的想,对于每个背包独有的糖果我们都取,如果超过了n/2,那么就取n/2个。然后对于两个背包共有的糖果,我们将其全取出来并去重,然后对于这些共有的糖果我们就放到个数小于n/2的背包中。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=3e6+10;
const int inf=0x3f3f3f3f;
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
//const ll P=2281701377;
const ll P=998244353;

set<int> s,ss,same;
void solve(){
	s.clear();
	ss.clear();
	same.clear();
	int n,x,y;
	cin>>n>>x>>y;
	while(x--){
		int a,b;cin>>a>>b;
		s.insert(a);
	}
	while(y--){
		int a,b;cin>>a>>b;
		ss.insert(a);
		if(s.find(a)!=s.end()){
			same.insert(a);
		}
	}
	int ls=s.size(),lsa=same.size(),lss=ss.size();
	cout<<min(min(ls-lsa,n/2)+min(lss-lsa,n/2)+lsa,n)<<endl;
}
int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--){
        solve();
    }

}


I 题

        由定义可知,对于一个点,他最多只可以连接一个其他点,最少就是不选本身点,我们考虑树形dp。0表示不选本身,1表示选了本身但不连子节点,2表示选了本身且连了一个子节点。

        对于0,由于不选本身,那么对于所以符合条件的子树状态都可以选,可以发现是满足分步乘法的。对于1,因为不连接子节点,我们可以转化成选了子节点的0状态,可以发现也是满足分步乘法的。对于2,我们只可以连接一个子节点,且该子节点不能连接其他子节点,也就是该子节点的1状态,但同时我们又要保证其他子节点不能选也就是0状态,可以发现是分类加法。

        对于2的其他子节点都不选也就是子节点的0的乘积,实际上就是当前节点的0。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
const int inf=0x3f3f3f3f;
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
//const ll P=2281701377;
const ll P=998244353;
#define int ll

int n,dp[N][4];
vector<int> v[N];
int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=res*a%P;
		b>>=1;
		a=a*a%P;
	}
	return res%P;
}
void dfs(int x,int f){
	dp[x][0]=dp[x][1]=1;
	dp[x][2]=0;
	for(auto j:v[x]){
		if(f==j) continue;
		dfs(j,x);
		dp[x][0]=dp[x][0]*(dp[j][0]+dp[j][1]+dp[j][2])%P;
		dp[x][1]=dp[x][1]*dp[j][0]%P;
	}
	for(auto j:v[x]){
		if(j==f) continue;
		dp[x][2]=(dp[x][2]+dp[x][1]*qpow(dp[j][0],P-2)%P*dp[j][1]%P)%P;
	}
}
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++)
		v[i].clear();
	for(int i=1;i<n;i++){
		int a,b;cin>>a>>b;
		v[a].push_back(b);
		v[b].push_back(a);
	}
	dfs(1,0);
	cout<<(dp[1][0]+dp[1][1]+dp[1][2])%P<<endl;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    while(t--){
        solve();
    }

}


K 题

        可以用更简单的差分的差分,但我没用。我们考虑贡献,每条操作对每个点的贡献。

        我们将每个操作的贡献分成两部分,一部分是从操作的左边到操作的中间,另一部分是从操作的右边到操作的中间+1,对于第二部分我们只要从后往前遍历执行第一部分的操作即可。

        我们可以发现对于第一部分,每个操作从左到右的贡献是+1的。我们设到当前仍然在作用域下的操作条数为cnt,那么到当前这个点他们的贡献为前一点的贡献加上cnt。

        我们遍历数组,如果当前位置是某个操作的起始位置,我们就加入这个条操作,如果当前位置超过某个操作的作用域了,我们就删掉当前操作。

        由于我们需要找到这些已经加入操作的作用域最小的操作,所以我们用优先队列来找作用域最小的操作。

        对于不同的操作,也就是说加操作和减操作,我们要分别记录他们的贡献,最后他们对某点的贡献就是加操作的贡献减去减操作的贡献。

        由于操作的作用域会超出【1,n】的数组范围,有两个解决办法,其中一个比较简单,就是对区间和操作加个偏移量,但我没用。另一个办法是对每个作用域越界的操作加个base属性,表示该操作起作用时的起始贡献。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int inf=0x3f3f3f3f;
const int N=1e6+10;
const int mod=1e9+7;
#define fi first
#define se second
#define int ll

int n,m;
int a[N],b[N];
struct node{
	int v,l,ks,base;
	bool operator <(const struct node&a)const{
		if(this->l+this->ks-1!=a.l+a.ks-1) return this->l+this->ks>a.l+a.ks;
		if(this->ks!=a.ks) return this->ks<a.ks;
		return this->v<a.v;
	}
};
struct nod{
	int v,l,js,base;
	bool operator <(const struct nod&a)const{
		if(this->js-this->l!=a.js-a.l) return this->js-this->l<a.js-a.l;
		if(this->js!=a.js) return this->js<a.js;
		return this->v<a.v;
	}
};
vector<node>v[N];
vector<nod> vv[N];
void solve(){
	cin>>n>>m;
	while(m--){
		int op,pos,vl;
		cin>>op>>pos>>vl;
		int l=(pos-vl+1);
		if(l<1){
			v[1].push_back({op,pos,1,1-l+1});
		}
		else{
			v[l].push_back({op,vl,l,1});
			
		}
		int r=pos+vl-1;
		if(r>n){
			vv[n].push_back({op,n-pos,n,r-n+1});
		}
		else{
			vv[r].push_back({op,vl-1,r,1});
		}
	}
	int sum1=0,cnt1=0,sum2=0,cnt2=0;
	priority_queue<node> q;
	priority_queue<nod> qq;
	for(int i=1;i<=n;i++){
		while(q.size()&&q.top().l+q.top().ks-1<i){
			int op=q.top().v,len=q.top().l,base=q.top().base;
			if(op==1){
				cnt1--;
				sum1-=len+base-1;
			}
			else{
				cnt2--;
				sum2-=len+base-1;
			}
			q.pop();
		}
		sum1+=cnt1;
		sum2+=cnt2;
		for(auto x:v[i]){
			int op=x.v,base=x.base;
			if(x.l==0) continue;
			if(op==1){
				cnt1++;
				sum1+=base;
			}
			else{
				cnt2++;
				sum2+=base;
			}
			q.push({x});
		}
		a[i]+=sum1-sum2;
	}
	cnt1=cnt2=sum1=sum2=0;
	for(int i=n;i>=1;i--){
		while(qq.size()&&qq.top().js-qq.top().l+1>i){
			int op=qq.top().v,len=qq.top().l,base=qq.top().base;
			if(op==1){
				cnt1--;
				sum1-=len+base-1;
			}
			else{
				cnt2--;
				sum2-=len+base-1;
			}
			qq.pop();
		}
		sum1+=cnt1;
		sum2+=cnt2;
		for(auto x:vv[i]){
			int op=x.v,base=x.base;
			if(x.l==0) continue;
			if(op==1){
				cnt1++;
				sum1+=base;
			}
			else{
				cnt2++;
				sum2+=base;
			}
			qq.push({x});
		}
		a[i]+=sum1-sum2;
	}
	for(int i=1;i<=n;i++){
		if(i>1) cout<<' ';
		cout<<a[i];
	}
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int t=1;
	//cin>>t;
	while(t--){
		solve();
	}
	return 0;
}

L 题

        我们将相同的字符缩成一个字符,然后贡献就是原先连续个数乘字符分数。然后发现每次消字符的边界就是以当前点为中心的最大回文串,因为缩完点后没有连续的字符了,回文串一定是奇回文串,马拉车的时候不用对字符串进行插空。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
const int inf=0x3f3f3f3f;
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
//const ll P=2281701377;
const ll P=998244353;
#define int ll

map<char,int> score;
int n,m;
int v[N],sum[N];
int reallen;
int d[N];
string s,ss,sss;
void get_d(string s,int n){
	d[1]=1;
	for(int i=2,l,r=1;i<=n;i++){
		if(i<=r) d[i]=min(d[r-i+l],r-i+1);
		while(s[i-d[i]]==s[i+d[i]]) d[i]++;
		if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;
	}
}
void solve(){
	cin>>n>>m;
	cin>>ss;
	ss=' '+ss;
	ss=ss+'$';
	sss=' '+sss;
	for(int i=1;i<=m;i++){
		char x;cin>>x;
		int va;cin>>va;
		score[x]=va;
	}
	int cnt=1;
	for(int i=1;i<ss.size()-1;i++){
		if(ss[i]==ss[i+1]) cnt++;
		else{
			sss+=ss[i];
			v[++reallen]=cnt*score[ss[i]];
			cnt=1;
		}
	}
	for(int i=1;i<=reallen;i++){
		sum[i]=sum[i-1]+v[i];
	}
	/*s+='$';
	for(int i=1;i<=reallen;i++){
		s+='#';s+=sss[i];
	}*/
	//i*2
	get_d(sss,sss.size()-1);
	int maxx=0;
	for(int i=1;i<=reallen;i++){
		int r=d[i]-1;
		maxx=max(maxx,sum[i+r]-sum[i-r-1]+score[sss[i]]);
	}
	cout<<maxx;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
   // cin>>t;
    while(t--){
        solve();
    }

}


M 题

        两种办法,一种是遍历所有合理值,第二个是三分。

        前者:

        我们先将数组排序,然后排完序后的数的顺序就是最终顺序。对于等差数列我们常用的操作是将每个数减去与开始位置的差,最后让所有数相等。我们减完后,再对数组进行排序,二次排序完后我们先处理奇数前缀和,偶数前缀和,值的前缀和。

        我们假设要将他们变成x,先在数组中找到x的下标,那么x前的数都是小于x的,x后的数都是大于x的,我们要将他们变成x,分别考虑前后部分。首先,如果x是奇数我们要将数组中的偶数都变成奇数,需要乘以100作为花费,x是偶数则相反。然后我们再求出前部分离目标的差值和后部分离目标的差值,最后除以2就是花费。

        和一般的n个数变同一个数的最小花费有点区别,因为+-1的花费是100,+-2的花费是1,所以最优的状态不一定是数组的中点,但也有相似的结论,一般问题对于中点区间的点和区间两头是等价的,我们考虑这个问题时将区间拓展到l-1,l,l+1,r-1,r,r+1即可,求出所有可能区间边界点,然后按上述方法求值即可。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
const int inf=0x3f3f3f3f;
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
//const ll P=2281701377;
const ll P=998244353;
#define int ll


int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=res*a%P;
		b>>=1;
		a=a*a%P;
	}
	return res%P;
}

int n,a[N];
int sum1[N],sum2[N],sum[N];
int get_res(int x){
	int res=0;
	int pos=lower_bound(a+1,a+1+n,x)-a;
	int presum1=sum1[pos-1]-sum1[0];
	int presum2=sum2[pos-1]-sum2[0];
	int nexsum1=sum1[n]-sum1[pos-1];
	int nexsum2=sum2[n]-sum2[pos-1];
	int presum=sum[pos-1]-sum[0];
	int nexsum=sum[n]-sum[pos-1];
	int needpresum=x*(pos-1);
	int neednexsum=x*(n-pos+1);
	if(x&1){
		res+=(presum2+nexsum2)*100;
		int needgetpre=needpresum-presum2-presum;
		int needgetnex=nexsum-nexsum2-neednexsum;
		res+=(needgetpre+needgetnex)/2;
	}
	else{
		res+=(presum1+nexsum1)*100;
		int needgetpre=needpresum-presum1-presum;
		int needgetnex=nexsum-nexsum1-neednexsum;
		res+=(needgetpre+needgetnex)/2;
	}
	return res;
}
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++){
		a[i]-=2*(i-1);
	}
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++){
		sum[i]=sum[i-1]+a[i];
		sum1[i]=sum1[i-1];
		sum2[i]=sum2[i-1];
		if(a[i]&1) sum1[i]++;
		else sum2[i]++;
	}
	set<int>s;
	for(int i=1;i<=n;i++){
		for(int k=-1;k<=1;k++)
			s.insert(a[i]+k);
	}
	int ans=0x3f3f3f3f3f3f3f3f;
	for(auto x:s){
		ans=min(ans,get_res(x));
	}
	cout<<ans<<endl;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    while(t--){
        solve();
    }

}


        后者:

        与一般的三分有所不同,因为奇数偶数操作花费不一样,所以我们考虑分奇数偶数三分。

 

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
const int inf=0x3f3f3f3f;
typedef long long ll;
typedef pair<int,int> pii;
typedef unsigned long long ull;
//const ll P=2281701377;
const ll P=998244353;
#define int ll


int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=res*a%P;
		b>>=1;
		a=a*a%P;
	}
	return res%P;
}

int n,a[N];
int sum1[N],sum2[N],sum[N];
int get_res(int x){
	int res=0;
	int pos=lower_bound(a+1,a+1+n,x)-a;
	int presum1=sum1[pos-1]-sum1[0];
	int presum2=sum2[pos-1]-sum2[0];
	int nexsum1=sum1[n]-sum1[pos-1];
	int nexsum2=sum2[n]-sum2[pos-1];
	int presum=sum[pos-1]-sum[0];
	int nexsum=sum[n]-sum[pos-1];
	int needpresum=x*(pos-1);
	int neednexsum=x*(n-pos+1);
	if(x&1){
		res+=(presum2+nexsum2)*100;
		int needgetpre=needpresum-presum2-presum;
		int needgetnex=nexsum-nexsum2-neednexsum;
		res+=(needgetpre+needgetnex)/2;
	}
	else{
		res+=(presum1+nexsum1)*100;
		int needgetpre=needpresum-presum1-presum;
		int needgetnex=nexsum-nexsum1-neednexsum;
		res+=(needgetpre+needgetnex)/2;
	}
	return res;
}
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++){
		a[i]-=2*(i-1);
	}
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++){
		sum[i]=sum[i-1]+a[i];
		sum1[i]=sum1[i-1];
		sum2[i]=sum2[i-1];
		if(a[i]&1) sum1[i]++;
		else sum2[i]++;
	}
	int ans=0x3f3f3f3f3f3f3f3f;
	for(int i=0;i<2;i++){
		int l=-1e9,r=1e9;
		while(l<r){
			int lmid=l+(r-l)/3;
			int rmid=r-(r-l)/3;
			int lres=get_res(lmid*2-i);
			int rres=get_res(rmid*2-i);
			if(lres>=rres) l=lmid+1;
			else r=rmid-1;
		}
		ans=min(ans,min(get_res(l*2-i),get_res(r*2-i)));
	}
	cout<<ans<<endl;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    while(t--){
        solve();
    }

}


代码

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值