CSP 202209题解:如此编码,何以包邮,防疫大数据,吉祥物投票,高维亚空间超频物质变压缩技术

试题内容请前往CCF官网查看:

CCF-CSP计算机软件能力认证考试
http://118.190.20.162/home.page

CCF 官方题解请点击这里。

阅读本题解前,您应当了解下列知识:

  1. 线段树 教程
  2. 并查集 教程
  3. C++ STL容器 教程
  4. 动态规划的斜率优化 教程
  5. CDQ分治 教程

这是一份以C++代码编写的CSP 专业组 202209题解。

请注意这是CSP-S/J的中学生竞赛的题解。

现将模拟测试系统中的得分列举如下:

题目 得分 时间 内存
如此编码 100 15ms 2.898MB
何以包邮 100 15ms 7.796MB
防疫大数据 100 500ms 34.66MB
吉祥物投票 100 171ms 17.42MB
高维亚空间超频物质变压缩技术 100 140ms 6.835MB

1. 如此编码

题目的提示已经非常充足了,没什么好说的。

#include
using namespace std;

int a[30],c[30],cb[30];

int main() {
	int n,m;c[0]=1;cb[0]=0;
	ios::sync_with_stdio(false);
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		c[i]=a[i]*c[i-1];
		cb[i]=m%c[i];
		cout<<(cb[i]-cb[i-1])/c[i-1]<<" ";
	}
	return 0;
}

2. 何以包邮?

标准的0-1背包,也没什么好说的。

如果您对“背包问题”了解甚少,您可以阅读崔添翼《背包九讲》。该链接指向原作者在Github上发布的文章,建议下载其中的pdf版本进行阅读。

#include
using namespace std;

int a[31];
int f[31][300001];
//f[i][j]表示考虑前i件物品,容积为j时最多能装的体积 

int main() {
	int n,x,sum=0;
	ios::sync_with_stdio(false);
	cin>>n>>x;
	for(int i=1;i<=n;i++) {
		cin>>a[i];
		sum+=a[i];
	}
	x=sum-x;
	for(int i=1;i<=n;i++) {
		for(int j=0;j<=x;j++) {
			if(j-a[i]>=0) 
				f[i][j]=max(f[i-1][j],f[i-1][j-a[i]]+a[i]);
			else
				f[i][j]=f[i-1][j];
		}
	}
	cout<<sum-f[n][x]<<endl;
	return 0;
}

3. 防疫大数据

按照题目要求细心的做即可,没什么难度。CSP专业组第3题通常用C++STL操作一下就可以搞定。

#include
using namespace std;

map<int,set<int> > risk_areas; //map[region]={risk_days}

void set_risk_area(int day,int area){
	auto it=risk_areas.find(area);
	if(it==risk_areas.end()) {
		risk_areas[area]=set<int>();
		it=risk_areas.find(area);
	}
	for(int i=0;i<7;i++) it->second.insert(day+i);
}

bool is_risk_area(int day_st,int day_ed,int area){
	auto it=risk_areas.find(area);
	if(it==risk_areas.end()) {
		return false;
	}else{
		for(int i=day_st;i<=day_ed;i++){
			if(it->second.find(i)==it->second.end()) return false;
		}
		return true;
	}
}

struct mdata{
	int d,u,r;
	void read(){
		cin>>d>>u>>r;
	}
	void print(){
		printf("{%d,%d,%d}",d,u,r);
	}
	bool operator<(const mdata &m)const{
		return d<m.d;
	}
	bool user_is_risk(int today){
		if(d<=today-7) return false; 
		return is_risk_area(d,today,r);
	}
};

multiset<mdata> routes_data;

void clear_expired_data(int today) {
	auto it=routes_data.lower_bound({today-6});
	routes_data.erase(routes_data.begin(),it);
}

void statistics(int today){
	set<int> users;
	clear_expired_data(today);
	for(auto itm:routes_data){
		//itm.print();
		if(itm.user_is_risk(today)){
			users.insert(itm.u);
		}
	}
	cout<<today<<' ';
	for(auto u:users){
		cout<<u<<' ';
	}
	cout<<endl;
}

int main() {
	int n,r,m,x;
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=0;i<n;i++){
		cin>>r>>m;
		for(int j=0;j<r;j++){
			cin>>x;
			set_risk_area(i,x);
		}
		for(int j=0;j<m;j++){
			mdata md;md.read();
			routes_data.insert(md);
		}
		statistics(i);
	}
	return 0;
}

4. 吉祥物投票

测试数据1~9: 首先测试数据1-4的做法是显然的,此处不再赘述。我们关注测试数据5-7。鉴于 n n n可达 1 0 9 10^9 109的范围,而总操作次数 q q q不超过 1 0 5 10^5 105,因此我们应当分段记录每个人的投票。我们可以用 ( l , r , x ) (l,r,x) (l,r,x)表示编号 l l l r r r的投票者投给 x x x号作品,这样的三元组显然不会超过 q q q个。下面的代码用pair >来描述3元组关系(具体使用map >实现),用cnt[x]记录当前投票给作品 x x x的人数:

#include
using namespace std;

struct seg{
	int l,r,x;
	int len(){return r-l+1;}
	seg(){}
	seg(pair<int,pair<int,int> > p):l(p.first),r(p.second.first),x(p.second.second){}
};

const int MAXM=1e5+10;
map<int,pair<int,int> > st;
int cnt[MAXM];

//操作1:请注意这不是最优代码,实际上会导致相邻两段的x相同而不合并他们的问题
void modify(int l,int r,int x){
	auto it=st.upper_bound(l);--it;
	seg last(*it);
	while(it!=st.end()){
		st.erase(it);cnt[last.x]-=(last.r-last.l+1);
		if(last.l<l){
			st[last.l]={l-1,last.x};
			cnt[last.x]+=(l-last.l);
		}
		if(last.r>r){
			st[r+1]={last.r,last.x};
			cnt[last.x]+=(last.r-r);
			break;
		}
		it=st.upper_bound(l);seg=node(*it);
	}
	st[l]={r,x};cnt[x]+=(r-l+1);
}
//操作2
void alter(int x,int w){
	for(auto it=st.begin();it!=st.end();++it){
		if(it->second.second==x) it->second.second=w;
	}
	cnt[w]+=cnt[x];cnt[x]=0;
}
//操作3
void exchange(int x,int y){
	for(auto it=st.begin();it!=st.end();++it){
		if(it->second.second==x) it->second.second=y;
		else if(it->second.second==y) it->second.second=x;
	}
	swap(cnt[x],cnt[y]);
}

int main() {
	ios::sync_with_stdio(false);
	int n,m,q,op,l,r,x,w,y;
	cin>>n>>m>>q;st[1]={n,0};cnt[0]=n;
	while(q--){
		cin>>op;
		if(op==1) {
			cin>>l>>r>>x;
			modify(l,r,x);
		}else if(op==2){
			cin>>x>>w;
			alter(x,w);
		}else if(op==3){
			cin>>x>>y;
			exchange(x,y);
		}else if(op==4){
			cin>>w;
			cout<<cnt[w]<<endl;
		}else{//op==5
			int maxv=cnt[1],wk=1;
			for(int i=2;i<=m;++i){
				if(cnt[i]>maxv) maxv=cnt[i],wk=i;
			}
			if(cnt[0]==n) wk=0;
			cout<<wk<<endl;
		}
	}
	return 0;
}

尽管上述代码中并未特别考虑数据8~9,但由于 m = 1 m=1 m=1的缘故,每次操作的实际用时较小(特别是操作5),因此也能通过这两个数据点。尽管 q q q较大,但均摊后不会每次修改都涉及所有段。(但是操作2,3确实会涉及所有段,因此确实是数据比较弱。)

数据10~11: 由于操作1时间复杂度均摊后为 O ( 1 ) O(1) O(1) (这里不确定),操作4时间复杂度为 O ( 1 ) O(1) O(1),而每次操作5都是 O ( m ) O(m) O(m)的,因此我们在cnt[]数组上套一个线段树,使其复杂度降至 O ( log ⁡ m ) O(\log m) O(logm)。当然,这样1,4操作的复杂度也会变成 O ( l o g m ) O(log m) O(logm)。总体时间复杂度为 O ( q log ⁡ m ) O(q\log m) O(qlogm),可以接受。

下列示意代码中并不是一个常规的线段树,其单点更新与常规的线段树略有区别。

#include
using namespace std;

const int MAXM=1e5+10;
int n,m;

struct node{//线段树节点
	int l,r,mx,mxpos;
	node *lc,*rc,*fa;
	node(node* f):fa(f){}
};

class seg_tree{//线段树
	node *rt;
	vector<int> v;
	vector<node*> ptr; 
	void __build(node* rt,int l,int r){
		rt->l=l;rt->r=r;rt->mx=0;rt->mxpos=l;
		if(l==r) {
			ptr[l]=rt;
			rt->lc=rt->rc=NULL;
			return;
		}
		int m=l+r>>1;
		rt->lc=new node(rt);rt->rc=new node(rt);
		__build(rt->lc,l,m);
		__build(rt->rc,m+1,r);
	}
	void __upd(node *p,int val){
		p->mx=val;int i=p->l;
		while(p->fa!=NULL){
			node* ac=(p==p->fa->lc)?p->fa->rc:p->fa->lc;
			if(ac==NULL || p->mx>ac->mx) p->fa->mx=p->mx,p->fa->mxpos=p->mxpos;
			else if(p->mx==ac->mx) p->fa->mx=p->mx,p->fa->mxpos=min(p->mxpos,ac->mxpos);
			else p->fa->mx=ac->mx,p->fa->mxpos=ac->mxpos;
			p=p->fa;
		}
	}
public:
	void init(int n){
		v.resize(n+1,0); 
		ptr.resize(n+1,NULL);
		rt=new node(NULL);
		__build(rt,1,n);
	}
	void update(int i){
		if(i>0) __upd(ptr[i],v[i]);
	}
	int& operator[](int i) {return v[i];}
	int maxpos(){
		if(rt->mx>0) return rt->mxpos;
		return 0;
	}
}cnt;

struct seg{
	int l,r,x;
	int len(){return r-l+1;}
	seg(){}
	seg(pair<int,pair<int,int> > p):l(p.first),r(p.second.first),x(p.second.second){}
};

map<int,pair<int,int> > st;

void modify(int l,int r,int x){
	auto it=st.upper_bound(l);--it;
	seg last(*it);
	while(it!=st.end()){
		st.erase(it);cnt[last.x]-=(last.r-last.l+1);
		if(last.l<l){
			st[last.l]={l-1,last.x};
			cnt[last.x]+=(l-last.l);
		}
		cnt.update(last.x);
		if(last.r>r){
			st[r+1]={last.r,last.x};
			cnt[last.x]+=(last.r-r);
			cnt.update(last.x);
			break;
		}
		it=st.upper_bound(l);last=seg(*it);
	}
	st[l]={r,x};cnt[x]+=(r-l+1);cnt.update(x);
}

void alter(int x,int w){
	for(auto it=st.begin();it!=st.end();++it){
		if(it->second.second==x) it->second.second=w;
	}
	cnt[w]+=cnt[x];cnt[x]=0;
	cnt.update(x);cnt.update(w);
}

void exchange(int x,int y){
	for(auto it=st.begin();it!=st.end();++it){
		if(it->second.second==x) it->second.second=y;
		else if(it->second.second==y) it->second.second=x;
	}
	swap(cnt[x],cnt[y]);
	cnt.update(x);cnt.update(y);
}

int main() {
	ios::sync_with_stdio(false);
	int q,op,l,r,x,w,y;
	cin>>n>>m>>q;
	cnt.init(m);cnt[0]=n;
	st[1]={n,0};
	while(q--){
		cin>>op;
		if(op==1) {
			cin>>l>>r>>x;
			modify(l,r,x);
		}else if(op==2){
			cin>>x>>w;
			alter(x,w);
		}else if(op==3){
			cin>>x>>y;
			exchange(x,y);
		}else if(op==4){
			cin>>w;
			cout<<cnt[w]<<endl;
		}else{//op==5
			cout<<cnt.maxpos()<<endl;
		}
	}
	return 0;
}

完整解答: 在上一解决方案的基础上,我们容易发现性能瓶颈在于操作2和操作3,它们每次都会修改所有投票段。我们不妨定义作品的“编号”和“虚拟编号”的概念。开始时,所有的编号和虚拟编号都相同。我们的操作1~5中输入的都是“虚拟编号”,因此输入时将“虚拟编号”先变回“编号”。当操作2发生时,我们记虚拟编号 x , w x,w x,w的实际编号记为 x ′ , w ′ x',w' x,w,则利用并查集 w ′ w' w并入编号 x ’ , x’, x,,并为虚拟编号 w w w指定一个新的实际编号(例如 n + 1 n+1 n+1);当操作4发生时,我们记虚拟编号 x , y x,y x,y的实际编号记为 x ′ , y ′ x',y' x,y,我们交换 x ′ , y ′ x',y' x,y使得 x x x指向 y ′ y' y y y y指向 x ′ x' x。这样,我们就不需要实际修改投票段中的内容了。

#include
using namespace std;

const int MAXM=1e5+10;
int n,m;

//基于并查集的编号-虚拟编号映射器 
class ufs_map{
	int n;
	vector<int> fa,mp,rmp;
	//fa:每个点属于的集合; mp:虚拟id(1~n)到 真实id(1~n+q)的映射; rmp:真实id(1~n+q)到虚拟id的映射 
	int find(int x){
		return x!=fa[x]?(fa[x]=find(fa[x])):x;
	}
public:
	void init(int n){
		this->n=n;fa.resize(n+1);mp.resize(n+1);rmp.resize(n+1);
		for(int i=0;i<=n;++i)fa[i]=i,mp[i]=i,rmp[i]=i;
	}
	int operator[](int x){return mp[x];}//将虚拟编号映射为实际编号 
	int map_back(int x){return rmp[find(x)];}//将实际编号映射为虚拟编号 
	void exchange(int u,int v){
		swap(rmp[mp[u]],rmp[mp[v]]);//交换虚拟id
		swap(mp[u],mp[v]);//交换真实id 
	}
	void merge(int v,int u){ 
		fa[find(mp[v])]=find(mp[u]);//合并v的真实id到u的真实id 
		rmp[mp[v]]=-1;
		fa.push_back(++n);//为v创建新的真实id
		rmp.push_back(v);mp[v]=n;
	}
}uf;

//线段树节点 
struct node{
	int l,r,mx,mxpos;
	node *lc,*rc,*fa;
	node(node* f):fa(f){}
};
//线段树
class seg_tree{
	node *rt;
	vector<int> v;
	vector<node*> ptr; 
	void __build(node* rt,int l,int r){
		rt->l=l;rt->r=r;rt->mx=0;rt->mxpos=l;
		if(l==r) {
			ptr[l]=rt;
			rt->lc=rt->rc=NULL;
			return;
		}
		int m=l+r>>1;
		rt->lc=new node(rt);rt->rc=new node(rt);
		__build(rt->lc,l,m);
		__build(rt->rc,m+1,r);
	}
	void __upd(node *p,int val){
		p->mx=val;int i=p->l;
		while(p->fa!=NULL){
			node* ac=(p==p->fa->lc)?p->fa->rc:p->fa->lc;
			if(ac==NULL || p->mx>ac->mx) p->fa->mx=p->mx,p->fa->mxpos=p->mxpos;
			else if(p->mx==ac->mx) p->fa->mx=p->mx,p->fa->mxpos=min(p->mxpos,ac->mxpos);
			else p->fa->mx=ac->mx,p->fa->mxpos=ac->mxpos;
			p=p->fa;
		}
	}
public:
	void init(int n){
		v.resize(n+1,0); 
		ptr.resize(n+1,NULL);
		rt=new node(NULL);
		__build(rt,1,n);
	}
	void update(int i){
		if(i>0) __upd(ptr[i],v[i]);
	}
	int& operator[](int i) {return v[i];}
	int maxpos(){
		if(rt->mx>0) return rt->mxpos;
		return 0;
	}
}cnt;

struct seg{
	int l,r,x;
	int len(){return r-l+1;}
	seg(){}
	seg(pair<int,pair<int,int> > p):l(p.first),r(p.second.first),x(p.second.second){}
};
map<int,pair<int,int> > st;

void modify(int l,int r,int tx){
	int x=uf[tx];//获取tx的真实id
	auto it=st.upper_bound(l);--it;
	seg last(*it);
	while(it!=st.end()){
		int mb_lastx=uf.map_back(last.x);
		st.erase(it);cnt[mb_lastx]-=(last.r-last.l+1);
		if(last.l<l){
			st[last.l]={l-1,last.x};
			cnt[mb_lastx]+=(l-last.l);
		}
		cnt.update(mb_lastx);
		if(last.r>r){
			st[r+1]={last.r,last.x};
			cnt[mb_lastx]+=(last.r-r);
			cnt.update(mb_lastx);
			break;
		}
		it=st.upper_bound(l);last=seg(*it);
	}
	st[l]={r,x};cnt[tx]+=(r-l+1);cnt.update(tx);
}

void alter(int x,int w){
	uf.merge(x,w);
	cnt[w]+=cnt[x];cnt[x]=0;
	cnt.update(x);cnt.update(w);
}

void exchange(int x,int y){
	uf.exchange(x,y);
	swap(cnt[x],cnt[y]);
	cnt.update(x);cnt.update(y);
}

int main() {
	ios::sync_with_stdio(false);
	int q,op,l,r,x,w,y;
	cin>>n>>m>>q;
	cnt.init(m);cnt[0]=n;
	st[1]={n,0};uf.init(m);
	while(q--){
		cin>>op;
		if(op==1) {
			cin>>l>>r>>x;
			modify(l,r,x);
		}else if(op==2){
			cin>>x>>w;
			alter(x,w);
		}else if(op==3){
			cin>>x>>y;
			exchange(x,y);
		}else if(op==4){
			cin>>w;
			cout<<cnt[w]<<endl;
		}else{//op==5
			cout<<cnt.maxpos()<<endl;
		}
	}
	return 0;
}

5. 高维亚空间超频物质变压缩技术

子任务1: 暴力枚举分段点,然后对每一段逐个判断。期望得分:10分,代码略。时间复杂度: O ( n 2 n ) O(n2^n) O(n2n)

子任务2: 简单的动态规划。

s i = v 1 + v 2 + . . . v i s_i=v_1+v_2+...v_i si=v1+v2+...vi(前缀和), f i f_i fi为只考虑前 i i i块黄金时的最小费用,显然 i i i号黄金就是最后一段的编号最大的黄金。为了方便描述,我们虚构一个0号黄金,并规定它自成一段,费用为0,神秘学质量 m 0 m_0 m0也为0,因此 f 0 = 0 f_0=0 f0=0

下面考虑状态转移方程,如果我们可以确定上一段的最后一块黄金编号为 j j j,那么 f i = f j + ( s i − s j − L ) 2 f_i=f_j+(s_i-s_j-L)^2 fi=fj+(sisjL)2。但实际上 j j j不能立刻确定,其取值范围是 { j ∣ 0 ≤ j < i , m j < m i , i , j ∈ N } \{j|0\le j{j∣0j<i,mj<mi,i,jN},因此我们可以在取值范围内枚举来确定它,即 f i = min ⁡ 0 ≤ j < i , m j < m i { f j + ( s i − s j − L ) 2 } f_i=\min_{0\le j< i,m_jfi=0j<i,mj<mimin{fj+(sisjL)2}

期望得分:40分,代码如下。时间复杂度: O ( n 2 ) O(n^2) O(n2)记得开long long

#include
using namespace std;

typedef long long ll;
const int MAXN=1e5+10;

int s[MAXN];//前缀和
int m[MAXN];//神秘学质量 

ll sqr(ll x){return x*x;}
ll f[MAXN];//f[i]表示最后一段以第i个物品结尾时的总成本

int main(){
	ios::sync_with_stdio(false);
	int n,L,x;
	cin>>n>>L;
	for(int i=1;i<=n;++i) {cin>>x;s[i]=s[i-1]+x;}
	for(int i=1;i<=n;++i) {cin>>m[i];f[i]=LONG_LONG_MAX;}
	f[0]=0;
	for(int i=1;i<=n;++i)
		for(int j=0;j<i;++j)//枚举最后一段的开头,上一段的结尾即j-1 
			if(m[i]>m[j]) 
				f[i]=min(f[i],f[j]+sqr(s[i]-s[j]-L)); //更优时更新
	cout<<f[n]<<endl;
	return 0;
}

子任务3: 由于保证 m i = i m_i=i mi=i,因此任意分割都满足编号最大的黄金神秘学质量递增。此时考虑动态规划的斜率优化,可以参考HNOI2008玩具装箱。以下进行简要推导。

首先,该子任务的状态转移方程为 f i = min ⁡ 0 ≤ j < i { f j + ( s i − s j − L ) 2 } f_i=\min_{0\le jfi=0j<imin{fj+(sisjL)2}
注意这里去掉了 m j < m i m_jmj<mi。在该子任务中满足所有 m i = i m_i=i mi=i,因此在 m j < m i m_jmj<mi j < i jj<i时已经自然满足。

对状态转移方程进行变形,将与 j j j的项提出最小值函数,得
f i − ( s i − L ) 2 = min ⁡ 0 ≤ j < i { f j + s j 2 + 2 L s j − 2 s i s j } f_i-(s_i-L)^2=\min_{0\le jfi(siL)2=0j<imin{fj+sj2+2Lsj2sisj}
不妨记 x j = s j , y j = f j 2 + s j 2 + 2 L s j , k i = 2 s i , b i = f i − ( s i − L ) 2 x_j=s_j,y_j=f_j^2+s_j^2+2Ls_j,k_i=2s_i,b_i=f_i-(s_i-L)^2 xj=sj,yj=fj2+sj2+2Lsj,ki=2si,bi=fi(siL)2,则有 b i = min ⁡ 0 ≤ j < i { y j − k i x j } b_i=\min_{0\le jbi=0j<imin{yjkixj}

将每个 j j j所对应的 ( x j , y j ) (x_j,y_j) (xj,yj)看作平面直角坐标系内的一点, k i k_i ki看作斜率(在 i i i确定时即为常数),所求即为直线 y j = k i x j + b i y_j=k_ix_j+b_i yj=kixj+bi的截距的最小值,如下图直线所示。
CSP 202209题解:如此编码,何以包邮,防疫大数据,吉祥物投票,高维亚空间超频物质变压缩技术_第1张图片
(根据CC BY-SA 4.0协议,图片引用自此页面)

容易发现,使得截距取得最小值的 ( x j , y j ) (x_j,y_j) (xj,yj)必然位于点集的下凸壳上,并且是下凸壳上第一个与前一点所连线段之斜率大于 k i k_i ki的点。

注意到每次有新点加入凸壳时,其横坐标是递增的,而每次查询时所用的斜率也是递增的。这种双递增的特性使得我们可以使用单调队列来实现这个凸壳的维护。

#include
using namespace std;

typedef long long ll;
typedef long double ld;
const int MAXN=1e5+10;

int que[MAXN],head,tail; 
ll s[MAXN];//前缀和
int m[MAXN];//神秘学质量 
ll f[MAXN],L;//f[i]表示最后一段以第i个物品结尾时的总成本

inline ll sqr(ll x){return x*x;}
inline ll X(int j){return s[j];}
inline ll K(int i){return 2ll*s[i];}
inline ll Y(int j){return f[j]+s[j]*(s[j]+2*L);}
inline ld slope(int i,int j) {return (Y(i)-Y(j))/(ld)(X(i)-X(j));}

int main(){
	ios::sync_with_stdio(false);
	int n,x;
	cin>>n>>L;
	for(int i=1;i<=n;++i) {cin>>x;s[i]=s[i-1]+x;}
	for(int i=1;i<=n;++i) {cin>>m[i];}
	
	f[0]=0;	que[head=tail=1]=0;
	
	for(int i=1;i<=n;++i) {
		while(head<tail && slope(que[head],que[head+1])<=K(i)) ++head;
		int j=que[head];
		cout<<j<<" "<<endl;
		f[i]=f[j]+sqr(s[i]-s[j]-L);
		while(head<tail && slope(que[tail],i)<=slope(que[tail-1],que[tail])) --tail;
		que[++tail]=i;
	}
	cout<<f[n]<<endl;
	return 0;
}

子任务4: 在子任务3的基础上,我们不再有 m i = i m_i=i mi=i这一条件。事实上,对于状态数为 O ( n ) O(n) O(n),每个状态转移时间为 O ( n ) O(n) O(n)的动态规划算法,一种常见的做法是CDQ分治,可以将整体的复杂度降低至 O ( n log ⁡ 2 n ) O(n\log^2n) O(nlog2n)

所谓CDQ分治,是2008年中国信息学竞赛国家队选手陈丹琪(Chen Danqi)在集训期间归纳的一种针对具有二维偏序特征序列的一种分治算法。详情请见这里,论文原稿请点这里。

对于本题,其动态规划方程如下: f i = min ⁡ 0 ≤ j < i , m j < m i { f j + ( s i − s j − L ) 2 } f_i=\min_{0\le j< i,m_jfi=0j<i,mj<mimin{fj+(sisjL)2}

我们已经通过斜率优化降低了因 j < i jj<i这一限制导致的动态规划转移时间复杂度。我们现在考虑,对原序列适当的重新排列,使得序列可以分为前后两部分:前一部分中的每一块黄金,其神秘学质量均小于后一部分中任意一块黄金。 在这种情况下,假定前一部分的黄金的代价已经计算完毕,那么我们可以继续使用斜率优化的方法,用前一部分黄金的 f [ ] f[] f[]去优化后一部分的黄金的 f [ ] f[] f[]。在此之后,我们进一步优化后一部分黄金的 f [ ] f[] f[]

一种边界情形是:序列中只有一种神秘学质量,那么此时无可优化,直接结束。

记我们要考虑的神秘学质量的最小值和最大值分别为 l , r l,r l,r,那么我们可以写出如下分治伪代码:

SOLVE(l,r)
  if l==r
    return
  m=(l+r)/2
  SOLVE(l,m)
  基于斜率优化,用前一部分黄金的f[]更新后一部分黄金的f[]
  SOLVE(m+1,r)

关于这种分治方法的正确性的方法的说明,请见上方的CDQ分治教程链接。

在代码实现上,我们需要考虑如何快速得到哪些黄金的神秘学质量落在 [ l , r ] [l,r] [l,r]的范围内。一个便利的做法是,我们将原来的黄金按神秘学质量重新排序(同时记住他们的原来的编号)。这样,当我们考虑 [ l , r ] [l,r] [l,r]范围内的黄金时,他们在数组中也是连续的,便于处理。但当我们做斜率优化时,我们必须将它们还原成原本的顺序,因为这是原本状态转移方程中的另一个要求: j < i jj<i。这种方法的实现代码如下:

#include
using namespace std;

typedef long long ll;
typedef long double ld;
const int MAXN=1e5+10;

int que[MAXN],head,tail;

struct gold{
	int id,mass; //编号,神秘学质量 
}a[MAXN];

//按编号排序
bool cmp_id(gold a,gold b){return a.id<b.id;}
//按质量排序
bool cmp_mass(gold a,gold b){return a.mass==b.mass?(a.id<b.id):(a.mass<b.mass);}

ll s[MAXN];//前缀和
ll f[MAXN];//f[i]表示最后一段以第i个物品结尾时的总成本
ll L;
inline ll sqr(ll x){return x*x;}
inline void update(int i,int j) {//用编号为j的黄金去更新编号为i的黄金
	f[i]=min(f[i],f[j]+sqr(s[i]-s[j]-L));
}
inline ll X(int j){return s[j];}
inline ll K(int i){return 2ll*s[i];}
inline ll Y(int j){return f[j]+s[j]*(s[j]+2*L);}
inline ld slope(int i,int j) {return (Y(i)-Y(j))/(ld)(X(i)-X(j));}

void solve( int l,//要考虑的神秘学质量下限 
			int r,//要考虑的神秘学质量上限 
			gold* lb,//神秘学质量下限l在数组a中的开始地址 
			gold* rb //神秘学质量上限r在数组a中的结束地址+1 
			) {
	if(l>=r) return; //l==r时,该点已经计算好了,不用再算了。 
	if(rb<=lb) return;
	int m=l+r>>1; 
	//用神秘学质量<=m的点去更新神秘学质量>m的点 
	//首先需要找到a数组中 m与 m+1的分界线的下标
	auto mb=upper_bound(lb,rb,(gold){MAXN,m},cmp_mass); //mb是第一个神秘学质量>m的下标
	solve(l,m,lb,mb); //先求解神秘学质量在l~m的位置
	//仿照子任务3,用单调队列维护lb~mb-1的点构成的下凸壳
	//由于目前按照神秘学质量排序,我们需要重新变回原来的输入顺序
	int cntl=mb-lb,cntr=rb-mb;//<=m的个数,>m的个数 
	//拷贝并变回输入顺序 
	vector<gold> tmpl(lb,mb),tmpr(mb,rb);
	sort(tmpl.begin(),tmpl.end(),cmp_id);
	sort(tmpr.begin(),tmpr.end(),cmp_id);
	//用斜率优化的方法更新 
	que[head=tail=1]=0;
	auto itl=tmpl.begin();
	for(auto itr=tmpr.begin();itr!=tmpr.end();++itr) {
		int i=itr->id;
		//开始更新f[i] 
		//编号小于待更新者进入单调队列
		for(;itl!=tmpl.end() && itl->id<i;++itl){
			int j=itl->id;
			while(head<tail && slope(que[tail],j)<slope(que[tail-1],que[tail])) --tail;
			que[++tail]=j;
		}
		//按照K(i)单调的原则,弹出队首小于K(i)的元素 
		while(head<tail && slope(que[head],que[head+1])<K(i)) ++head;
		//留下的队首用于更新f[i]
		update(i,que[head]);			
	}
	//最后求解神秘学质量在m+1~r的位置
	solve(m+1,r,mb,rb);
}

int main(){
	ios::sync_with_stdio(false);
	int n,x;
	cin>>n>>L;
	for(int i=1;i<=n;++i) {cin>>x;s[i]=s[i-1]+x;}
	for(int i=1;i<=n;++i) {cin>>a[i].mass;a[i].id=i;f[i]=LONG_LONG_MAX;}
	f[0]=0;	//初值f[0]=0,其他f[]均为无穷大
	sort(a+1,a+n+1,cmp_mass); //将点按照神秘学质量进行排序 
	solve(0,n,a+1,a+n+1); //以神秘学质量为偏序进行分治 
	cout<<f[n]<<endl;
	return 0;
}

作者本人在本次CSP测试中取得的成绩如下:
CSP 202209题解:如此编码,何以包邮,防疫大数据,吉祥物投票,高维亚空间超频物质变压缩技术_第2张图片


今日(2022年9月18日)参加CSP专业级认证,不甚理想,考前目标400,结果仅330(100+100+100+30+0),仔细想来,考试过程中出现诸多问题,在此一一列举,与诸位共勉。

过几天这篇帖子也许会改成一份题解。

做题的心路历程

T1:送分题。对照题目的提示,哐哐哐就出来了。但是由于我码字过慢,30min才写完交掉。 100分

T2:基础题。标准的0-1背包,简单DP一下就OK。但是由于我基础不牢,对自己的DP技术毫无自信,先去写了T3才来写T2,用时30min。 100分

T3:阅读理解题。由于题目太长太绕,不得不先花30min读题,边读边写代码,总体思想就是用STL一通乱搞,搞了4~5把过了。用时60min。100分

T4:不会做。感觉像线段树,但是发现怎么都搞不定。就这样折腾了60min。究其原因,是因为我既想维护每个人的选项,又想维护每个作品的得票数。而且这个操作1不会只影响某个作品的得票数,因为它实质上从别人那里抢票过来。所以感觉很难办。加之 n n n至多 1 0 9 10^9 109,线段树也只能对作品开。思路混乱,跑去看了T5。最后只写了20分的暴力,和10分的无2,3操作的 l − r ≤ 10 l-r\le 10 lr10,本来还想写有2,3操作的 l − r ≤ 10 和 l-r\le 10和 lr10m=1$的情况,来不及了。 感觉比往年的T4难一些。30分

T5:不会做。盯着题目看了20分钟,没有灵感,花了10min写10分暴力。一遍没搞定,慌了,赶紧跑去写T4暴力,最后T5爆0。

另外,考场电脑慢了5min,到了17:26想交一把,发现考试已结束。晕。说不定还能碰10分呢。

一点建议

前三题通常不难,建议按顺序做,60min内搞定。具体分配为10min+15min+35min。

剩下的时间集中攻关4,5题,而且一旦放弃某一题,就不要再看了。鄙人的经验是:反复横跳时哪一个都搞不定。个人感觉,以我自己的水平,上来对着T5写个暴力交掉就放弃T5,然后攻关T4尽可能多拿分比较合适。

你可能感兴趣的:(CSP专业组题解,其他)