241109 noip 模拟赛

省流:\(100+35+1+0\) 遗憾离场。

T1

题意:给一个长度为 \(n\) 的序列 \(a\),需要进行 \(q\) 次操作,每次给定一个 \(k\) 使 \(a_i = a_i + i \times k\) 操作后求出序列的最大值,强制在线。

\(n,q \leq 5 \times 10^5,1 \leq a_i,k \leq 10^{18}\),保证答案不超过 long long 的范围。

\(s_i\) 表示前 \(i\) 个操作的 \(k\) 的和,则此时第 \(i\) 个位置的值为 \(i \times s_i + a_i\),发现这是一个一次函数的形式,可以用单调栈维护下凸壳,由于 \(s_i\) 是递增的,所以用一个指针记录当前取得最大值的是哪个位置,把 \(s_i\) 代进去算一下就好了,时间复杂度 \(\Theta(n)\)

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
int T,n,id,q,a[N],st[N];
inline int pos(int x,int y) {
	if(!x) return 0;
	return (a[x]-a[y])/(y-x)+((a[x]-a[y])%(y-x)?1:0);
}
inline void write(int x) {
	int wr[20],tot=0;
	while(x) wr[++tot]=x%10,x/=10;
	for(int i=tot; i>=1; i--) putchar(wr[i]+'0');
	if(!tot) putchar('0');
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>T>>n>>id;
	while(T--) {
		cin>>q;
		int lim=LONG_LONG_MAX;
		for(int i=1; i<=n; i++) {
			cin>>a[i];
			lim=min(lim,(LONG_LONG_MAX-a[i])/i);
		}
		int top=0;st[++top]=0;
		for(int i=1; i<=n; i++) {
			while(top>1&&pos(st[top],i)<=pos(st[top-1],st[top])) top--;
			st[++top]=i;
		}
		vector<pair<pair<int,int>,int>> ve;
		for(int i=2; i<top; i++) ve.push_back(make_pair(make_pair(pos(st[i-1],st[i]),pos(st[i+1],st[i])-1),st[i]));
		ve.push_back(make_pair(make_pair(pos(st[top-1],st[top]),lim),st[top]));
		int ans=0,sum=0,pos=0,k;
		while(q--) {
			cin>>k;
			if(id) k=(k^(ans%(k+1)));
			sum+=k;
			while(pos<ve.size()&&ve[pos].first.second<sum) pos++;
			write(ans=a[ve[pos].second]+ve[pos].second*sum);putchar('\n');
		}
	}
	return 0;
}

闲话:win 7机子跑的是真的慢,考场测大样例跑了4s,虚空卡常1h,但不管怎么卡都是4s,最终出结果最慢点0.4s(

T2

原题 P8428。

题意:给定 \(n\) 个点的树,树上有 \(k\) 个关键点,你可以选取若干个点(可以是关键点),每个点可以覆盖离自己最近的关键点(如有多个则都可以覆盖)。最小化选取的点数,使得每个关键点都可以覆盖到,输出方案。

\(1 \leq k \leq n \leq 10^6\)

有一个贪心策略就是每次选择一个深度最大的关键点,如果它还没被覆盖,则找到深度最小的能够覆盖它的点,用这个点进行覆盖。证明个人感觉比较类似于今年 csp-s t2,虽然只有我这么认为

直接模拟就是 \(\Theta(n^2)\) 的,可以获得35的高分,我赛时就这么写的

考虑优化:预处理出来 \(dis_u\) 表示离这个点最近的关键点到它的距离,那么覆盖递归时,若 \(dis_v=dis_u + 1\) 并且 \(v\) 没有被覆盖过才访问 \(v\),否则不访问。证一下这个的正确性,由于覆盖的点是所有到这个点距离小于等于这个点的 \(dis\) 的点,所以如果 \(dis_v != dis_u + 1\) 说明继续往下递归不可能覆盖到关键点,所以不需要覆盖,而如果这个点已经被覆盖了,则它往下递归能够覆盖的点也已经全部被覆盖了,所以这样是正确的。可以画图感性理解一下。这样每个点至多被覆盖一次,均摊复杂度为 \(\Theta(n)\),预处理 \(dis\) 可以用多源 BFS,时间复杂度也是 \(\Theta(n)\),所以总时间复杂度是 \(\Theta(n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,k,x[N],dis[N],fa[N],d[N],vis[N],genshin[N],ans=0,head[N],ecnt=0;
struct edge {int to,nxt;}e[N<<1];
void add(int u,int v) {e[++ecnt]=(edge){v,head[u]};head[u]=ecnt;}
bool cmp(int a,int b) {return d[a]>d[b];}
void dfs1(int u,int pre,int dep) {
	fa[u]=pre,d[u]=dep;
	for(int i=head[u]; i; i=e[i].nxt) {
		int v=e[i].to;
		if(v==pre) continue;
		dfs1(v,u,dep+1);
	}
}
void dfs2(int u,int pre,int dist) {
	vis[u]=true;
	if(!dis) return;
	for(int i=head[u]; i; i=e[i].nxt) {
		int v=e[i].to;
		if(v==pre||dis[v]!=dist-1||vis[v]) continue;
		dfs2(v,u,dist-1);
	}
}
int main() {
	cin>>n>>k;
	for(int i=1; i<n; i++) {
		int u,v;
		cin>>u>>v;
		add(u,v),add(v,u);
	}
	memset(dis,0x3f,sizeof(dis));
	queue<int> q;
	for(int i=1; i<=k; i++) {
		cin>>x[i];
		dis[x[i]]=0;
		q.push(x[i]);
	}
	while(!q.empty()) {
		int u=q.front();
		q.pop();
		for(int i=head[u]; i; i=e[i].nxt) {
			int v=e[i].to;
			if(dis[v]>dis[u]+1) {
				dis[v]=dis[u]+1;
				q.push(v);
			}
		}
	}
	dfs1(1,0,1);
	sort(x+1,x+k+1,cmp);
	for(int i=1; i<=k; i++) {
		if(!vis[x[i]]) {
			int tmp=x[i];
			while(fa[tmp]&&dis[fa[tmp]]==dis[tmp]+1) tmp=fa[tmp];
			genshin[++ans]=tmp;
			dfs2(tmp,0,d[x[i]]-d[tmp]);
		}
	}
	cout<<ans<<endl;
	for(int i=1; i<=ans; i++) cout<<genshin[i]<<" ";
	return 0;
}

闲话:noip模拟t2放紫题素质呢/fn。不过这题也是真唐,不知道赛时在抽什么风,这都做不出。

T3

题意:给定一张 \(n\) 个点的图,判定该图是否是无自环的无向图且每个点的度数相同且无三元环。 同时用给定的 \(n\),构造每个点度数最大的,满足上述要求的图。

\(n \leq 1000\)

分类讨论。

第一问拿 bitset 搞就可以了,所以直接看第二问。 假设每个点最大的度数为 \(k\)

  • 结论一:不论 \(n\) 是多少,\(k \leq \frac{n}{2}\)

这个证明是容易的。考虑反证,假设某个点向外连了超过点数一半的边,那么与它联边的点最多只能 和少于一半的点联边,这就不能让度数相同了。

于是 为偶数的情况就解决了。只要把点均分成两份,每个点向另一份的所有点连边就好。这是二分图。

如果是奇数呢?

  • 结论二:如果 \(n\) 是奇数,则 \(k \leq \frac{2n}{5}\)

刚才提到了二分图,不妨按着这个思路想。

除去 \(k\) 特别小的情况,显然这就不是二分图。也就是说有奇环,假设是 \(L\)

  • 结论三:\(L\) 之外的点与 \(L\) 之内的点联边不超过两条。

反证,假设某个点 \(x\)\(L\) 内的点有三条边,假设连的点按环上的顺序分别是 \(a_1,a_2,a_3\)。这样就会形成三个环,分别是:

  • \(x \to a_1 \to \cdots \to a_2 \to x\)
  • \(x \to a_2 \to \cdots \to a_3 \to x\)
  • \(x \to a_3 \to \cdots \to a_1 \to x\)

显然,这三个环的长度之和为 \(\lvert L \rvert\)。显然这是奇数,所以必然有一个奇环两个偶环。这两个偶环长度和不小于 \(8\),所以这个奇环长度不大于 \(\lvert L \rvert - 2\),这就矛盾了。所以引理正确。

发现这个限制很强,从这个角度找证明。则有:

环上的点的度数之和 \(k \lvert L \rvert \leq 2 \lvert L \rvert + 2 (n - \lvert L \rvert)) = 2n\)

又因为 \(\lvert L \rvert > 3\)(题目限制),所以移项发现 。得证。

于是,我们只要构造出 \(k = \frac{2n}{5}\) 的解即可。题目又有限制:“含有数码 \(3\) 或数码 \(9\) ,你就会拒绝回答第二问”,也就是只要构造出 \(5n,5n + 1,5n + 2\) 即可。

  • \(5n\):把 \(n\) 分成 \(5\) 个部分,每对相邻的部分用二分图的连边方法两两连边即可。
  • \(5n + 1\):在 \(5n\) 的基础上把第一个部分减少一个点,第三和四个部分增加一个点,然后相邻的部分用二分图的连边方法两两连边,但是这样第三第四个部分会多出 \(1\) 的度数,使第三第四的部分每个点连的边都减少一个就行。
  • \(5n + 2\):同 \(5n + 1\) 把第一部分减少两个,第三第四增加两个,第三第四部分每个点连的边减少两个。

\(n = 1\)\(n = 7\) 的时候需要特判。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=2005;
char a[N][N];
vector<int> ve[10];
int t,n,c[N],ans[N][N];
bitset<N> b[N];
void addedge(int u,int v,int k) {
	if(!k) {
		for(int i=0; i<ve[u].size(); i++)
			for(int j=0; j<ve[v].size(); j++)
				ans[ve[u][i]][ve[v][j]]=ans[ve[v][j]][ve[u][i]]=1;
	} else if(k==1) {
		for(int i=0; i<ve[u].size(); i++) {
			for(int j=0; j<ve[v].size(); j++) {
				if(i==j) continue;
				ans[ve[u][i]][ve[v][j]]=ans[ve[v][j]][ve[u][i]]=1;
			}
		}
	} else {
		int len=ve[u].size();
		for(int i=0; i<len; i++) {
			for(int j=0; j<len; j++) {
				if(i==j||i==(j+1)%len) continue;
				ans[ve[u][i]][ve[v][j]]=ans[ve[v][j]][ve[u][i]]=1;
			}
		}
	}
}
bool check(int n) {while(n) {if(n%10==3||n%10==9) return true;n/=10;}return false;}
int main() {
	cin>>t;
	while(t--) {
		cin>>n;
		memset(c,0,sizeof(c));
	    for(int i=0;i<n;i++)for(int j=0;j<n;j++)cin>>a[i][j],a[i][j]-='0',b[i][j]=a[i][j],c[j]+=a[i][j];
	    bool valid=1;
	    for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(a[i][j]&&(b[i]&b[j]).count())valid=0;
	    for(int i=1;i<n;i++)if(b[i].count()!=b[1].count())valid=0;
	    for(int i=0;i<n;i++)for(int j=i;j<n;j++)if(a[i][j]!=a[j][i])valid=0;
	    if(valid)cout<<"Yes\n";
	    else cout<<"No\n";
	    for(int i=0;i<n;i++)b[i].reset();
		if(check(n)) {cout<<"Nope"<<endl;continue;}
		if(n%2==0) {
			cout<<n/2<<endl;
			for(int i=1; i<=n; i++) {
				for(int j=1; j<=n; j++) {
					if((i&1)!=(j&1)) cout<<1;
					else cout<<0;
				}
				cout<<endl;
			}
		} else if(n==1) {
			cout<<0<<endl<<0<<endl;
		} else if(n==7) {
			cout<<"2"<<endl;
			cout<<"0100001"<<endl;
			cout<<"1010000"<<endl;
			cout<<"0101000"<<endl;
			cout<<"0010100"<<endl;
			cout<<"0001010"<<endl;
			cout<<"0000101"<<endl;
			cout<<"1000010"<<endl;
		} else {
			int tmp=n/5;
			for(int i=1; i<=5; i++) {
				ve[i].clear();
				for(int j=(i-1)*(n/5)+1; j<=i*(n/5); j++) ve[i].push_back(j);
			}
			for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) ans[i][j]=0;
			cout<<2*tmp<<endl;
			if(n%5==0) {
				for(int i=1; i<5; i++) addedge(i,i+1,0);
				addedge(5,1,0);
			} else if(n%5==1) {
				int a=ve[1].back();ve[1].pop_back();
				ve[3].push_back(a),ve[4].push_back(n);
				addedge(1,2,0),addedge(2,3,0),addedge(3,4,1),addedge(4,5,0),addedge(5,1,0);
			} else {
				int a=ve[1].back();ve[1].pop_back();int b=ve[1].back();ve[1].pop_back();
				ve[3].push_back(a),ve[3].push_back(b),ve[4].push_back(n-1),ve[4].push_back(n);
				addedge(1,2,0),addedge(2,3,0),addedge(3,4,2),addedge(4,5,0),addedge(5,1,0);
			}
			for(int i=1; i<=n; i++,cout<<endl) for(int j=1; j<=n; j++) cout<<ans[i][j];
		}
	}
}

闲话:好难写啊/oh/oh/oh

T4

题意:给一个长度为 \(n\) 初始全为 \(0\) 的序列 \(a,s\) 和一个给定值的序列 \(b\),和一个模数 \(m\),有 \(q\) 次操作,操作形式如下:

第一步:

  • 1 l r d 表示给 \(a\) 在区间 \([l,r]\) 上加 \(d\)
  • 2 l r d 表示给 \(b\) 在区间 \([l,r]\) 全部改为 \(d\)

第二步:对于每个 \(i\) 使 \(s_{b_i} = (s_{b_i} + a_i) \% m\)

全部操作完毕后你需要输出 \((m - s_i) \% m\) 的值。

\(n \leq 10^6\)

用 ODT 维护 \(b\) 序列,用类似扫描线的操作,每次加入一个颜色段就减去当前这个区间的历史和,删除一个颜色段就加上这个区间的历史和,由于每次覆盖增加的颜色段至多为 \(2\),所以时间复杂度正确。

那么如何维护历史和呢?

设当前版本为 \(t\)\(hsum_i\) 表示第 \(i\) 个点的历史和,\(sum_i\) 表示第 \(i\) 个点的当前版本的值,那么再维护一个值 \(v = hsum_i - t \times sum_i\)。每次区间加 \(d\) 相当于给区间内的 \(sum_i + d\),给区间内的 \(v - t \times d\),要查询一个区间 \([l,r]\) 的历史和就是 \(hsum_{l,r} = v_{l,r} + t \times sum_{l,r}\),由于这样的拆的柿子非常巧妙,所以不需要考虑版本加一时 \(v\) 的值的变化。这样只需要实现一个区间加区间求和的数据结构就行了,你非常开心的写了个线段树,发现它t飞了,于是你学了一下树状数组的区间加区间求和,然后就过辣!

参考 @fangzichang 大神的文章,讲的是同一个题。

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,q,mod,t,b[N],sum[N];
struct node {
	int l,r,c;
	bool operator<(const node &o) const {return l<o.l;}
};set<node> s;
struct BIT {
	int t1[N],t2[N];
	int lowbit(int x) {return x&(-x);}
	void add(int p,int x) {int tmp=p;while(p<=n) (t1[p]+=x)%=mod,(t2[p]+=tmp*x%mod)%=mod,p+=lowbit(p);}
	int query(int p) {int ans1=0,ans2=0,tmp=p;while(p) (ans1+=t1[p])%=mod,(ans2+=t2[p])%=mod,p-=lowbit(p);return (ans1*(tmp+1)%mod-ans2+mod)%mod;}
	void add(int l,int r,int x) {add(l,x),add(r+1,mod-x);}
	int query(int l,int r) {return (query(r)-query(l-1)+mod)%mod;}
}t1,t2;
set<node>::iterator split(int x) {
	set<node>::iterator it=s.lower_bound((node){x,0,0});
	if(it!=s.end()&&(*it).l==x) return it;
	it--;
	if((*it).r<x) return s.end();
	int l=(*it).l,r=(*it).r,c=(*it).c;
	s.erase(it);
	s.insert((node){l,x-1,c});
	return s.insert((node){x,r,c}).first;
}
void assign(int l,int r,int c) {
	set<node>::iterator itr=split(r+1),itl=split(l);
	for(set<node>::iterator it=itl; it!=itr; it++) sum[(*it).c]=(sum[(*it).c]+t2.query((*it).l,(*it).r)+t*t1.query((*it).l,(*it).r)%mod)%mod;
	s.erase(itl,itr);
	s.insert((node){l,r,c});
	sum[c]=(sum[c]-t2.query(l,r)-t*t1.query(l,r)%mod+mod+mod)%mod;
}
signed main() {
	cin>>n>>q>>mod;
	for(int i=1; i<=n; i++) cin>>b[i],s.insert((node){i,i,b[i]});
	for(int &i=t=0; i<q; i++) {
		int op;
		cin>>op;
		if(op==1) {
			int l,r,d;
			cin>>l>>r>>d;d%=mod;
			t1.add(l,r,d),t2.add(l,r,mod-t*d%mod);
		} else {
			int l,r,d;
			cin>>l>>r>>d;
			assign(l,r,d);
		}
	}
	for(set<node>::iterator it=s.begin(); it!=s.end(); it++) sum[(*it).c]=(sum[(*it).c]+t2.query((*it).l,(*it).r)+t*t1.query((*it).l,(*it).r)%mod)%mod;
	for(int i=1; i<=n; i++) cout<<(mod-sum[i])%mod<<" ";
	return 0;
}

闲话:这题赶在 ABC 的前三分钟补完了,爽!

posted @ 2024-11-09 13:49  System_Error  阅读(79)  评论(0)    收藏  举报