CF 比赛 + 补题 + vp 记录

链接均用 ml,带 * 是没做出来的题,带 ** 是赛时根本没看的题,带 + 是个人比较喜欢的题目。

1. Codeforces Round #745 (Div. 1)

梦开始的地方,这个垃圾场让我决定板刷 + vp 一些 Codeforces 题目 / 比赛。

1580A. Portal *1700

枚举两行 \(i,j\),考虑列数 \(k\) 向右移动时对答案的影响:我们记 \(c_d(i,p,j,k)\ (i<j\land p<k)\) 表示将 \(M_{i,p}\sim M_{j,k}\) 刷成 \(d\) 的代价,那么将 \(M_{i,p}\sim M_{j,k}\) 刷成合法的 portal 的代价为

\[\begin{aligned}&c_0(i+1,1,j-1,k-1)\\-&c_0(i+1,1,j-1,p)\\+&c_1(i+1,p,j-1,p)\\+&c_1(i+1,k,j-1,k)\\-&c_1(i,1,i,p)\\+&c_1(i,1,i,k-1)\\-&c_1(j,1,j,p)\\+&c_1(j,1,j,k-1)\end{aligned} \]

前两项表示将内部刷成 \(0\) 的代价,后面六项表示将边框刷为 \(1\) 的代价。为了将 \(p,k\) 拆开来使用了差分。那么直接做就行了,时间复杂度 \(\mathcal{O}(n^2m)\)

// orz 1849285087
#include<bits/stdc++.h>
using namespace std;
const int N=400+5;
char mp[N][N];
int n,m,ans,f[N],s[N][N];
void solve(){
	cin>>n>>m,ans=1e9;
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)
		cin>>mp[i][j],s[i][j]=s[i-1][j]+mp[i][j]-'0';
	for(int i=1;i<=n;i++)
		for(int j=i+4;j<=n;j++){
			int pre=1e9,cur=0;
			for(int k=1;k<=m;k++){
				if(k>3)pre=min(pre,f[k-3]);
				int nd=s[j-1][k]-s[i][k];
				ans=min(ans,pre+cur+(f[k]=j-i-1-nd));
				cur+=nd+(mp[i][k]=='0')+(mp[j][k]=='0'),f[k]-=cur;
			}
		} cout<<ans<<endl;
}
int main(){
	int T; cin>>T;
	while(T--)solve();
	return 0;
}

+*1580B. Mathematics Curriculum *2600

\(n=100\) 是垃圾卡常题,\(n=40\) 是好题。

我们考虑对排列建出笛卡尔树,那么一个排列是合法的当且仅当它深度为 \(m\) 的节点个数为 \(k\)。因此考虑 DP,设 \(f_{i,j,k}\) 表示排列长度为 \(i\),深度为 \(j\) 的节点个数为 \(k\) 时合法的排列个数,转移就直接枚举左边排列的长度和节点数转移即可。

\[f_{i,j,k}=\sum_{p=0}^{i-1}\sum_{c=0}^kf_{p,j-1,c}\times f_{i-p-1,j-1,k-c}\times \dbinom{i-1}{p}\ (j\geq 2\land k\geq 1) \]

注意 \(f_{i,1,1}=i!\),且 \(f_{i,j,0}=i!-\sum_{k=1}^i f_{i,j,k}\)。时间复杂度 \(n^5\),加上部分剪枝能过。

#include <bits/stdc++.h>
using namespace std;

const int N=100+5;
int n,m,k,p,f[N][N][N],fc[N],C[N][N];
int main(){
	cin>>n>>m>>k>>p,fc[0]=1;
	for(int i=0;i<N;i++)f[0][i][0]=1;
	for(int i=0;i<N;i++)for(int j=0;j<=i;j++)
		C[i][j]=j==0||j==i?1:(C[i-1][j-1]+C[i-1][j])%p;
	for(int i=1;i<N;i++)fc[i]=1ll*fc[i-1]*i%p;
	for(int i=1;i<=n;i++){
		f[i][1][1]=fc[i];
		for(int j=2;j<=i;j++)for(int pos=0;pos<i;pos++)
            for(int c=0;c<=i;c++)if(f[pos][j-1][c])
			for(int k=c;k<=i;k++)if(f[i-pos-1][j-1][k-c])
				f[i][j][k]=(f[i][j][k]+1ll*f[pos][j-1][c]*f[i-pos-1][j-1][k-c]%p*C[i-1][pos])%p;
		for(int j=2;j<N;j++){
			f[i][j][0]=fc[i];
			for(int k=1;k<=i;k++)f[i][j][0]=(f[i][j][0]-f[i][j][k]+p)%p;
		}
	}
	cout<<f[n][m][k]<<endl;
	return 0;
}

1580C. Train Maintenance *2200

一个非常显然的根号重构题目。若 \(x+y\leq B\) 我们可以用桶记录其对 \(i\bmod (x+y)=d\) 的每个天数 \(i\) 的贡献。若 \(x+y>B\) 直接差分即可。注意取消差分贡献时下标对 \(i\)\(\max\),因为作用在 \(i\) 以前的位置 \(j\) 的差分需要在 \(i\) 处更新而不是 \(j\):你对差分数组位置 \(j\ (j+1<i)\) 的更新是不会在位置 \(i\) 中体现的,\(j\) 已经过时了。

时间复杂度 \(\mathcal{O}(\dfrac{nm}B+mB)\),取 \(B=\sqrt m\) 有最优复杂度 \(n\sqrt m\)。荣登 cf 最短代码榜首。

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,x[N],y[N],a[N],p[N],buc[555][555];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)scanf("%d%d",&x[i],&y[i]);
	for(int i=1;i<=m;i++){
		int op,k,c,ans=0;
		scanf("%d%d",&op,&k),c=x[k]+y[k];
		if(op==1){
			a[k]=i;
			if(c<555)for(int j=0;j<c;j++)buc[c][(i+j)%c]+=j>=x[k];
			else for(int j=i+x[k];j<=m;j+=c)p[j]++,p[min(m+1,j+y[k])]--;
		} else{
			if(c<555)for(int j=0;j<c;j++)buc[c][(a[k]+j)%c]-=j>=x[k];
			else for(int j=a[k]+x[k];j<=m;j+=c)p[max(i,j)]--,p[max(i,min(m+1,j+y[k]))]++;
		}
		for(int j=2;j<555;j++)ans+=buc[j][i%j];
		printf("%d\n",ans+(p[i]+=p[i-1]));
	}
	return 0;
}

教训:根号分治进入较大的分支调不出来时,试试将块大小设为 \(0\)

+*1580D. Subsequence *2900

我们建出笛卡尔树,然后就是裸的树形背包了啊。

时间复杂度 \(n^2\)。笛卡尔树可以直接递归建。

// orz chasedeath
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=4e3+5;
void cmax(ll &x,ll y){x=x>y?x:y;}
ll n,m,a[N],vis[N],f[N][N];
int solve(int l,int r){
	if(l>r)return 0;
	int p=l; for(int i=l;i<=r;i++)if(a[i]<a[p])p=i;
	int ls=solve(l,p-1),rs=solve(p+1,r);
	for(int i=0;i<=r-l+1;i++)f[p][i]=-1e18;
	for(int i=0;i<=p-l;i++)for(int j=0;j<=r-p;j++)
		cmax(f[p][i+j],f[ls][i]+f[rs][j]-i*j*2*a[p]),
		cmax(f[p][i+j+1],f[ls][i]+f[rs][j]-(i*j*2+i*2+j*2+1-m)*a[p]);
	return p;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	int r=solve(1,n);
	cout<<f[r][m]<<endl;
	return 0;
}

2. Codeforces Round #746 (Div. 2)

这一场用小号 Voc-9102 打,打到一半 rk1,然后被封号了,然后就弃赛了。

1592A. Gamer Hemose *800

直接选最大的两个武器贪心,就是从大到小排序,除以 \(a_1+a_2\) 看余数究竟为 \(0\) 还是 \(\leq a_1\) 还是 \(>a_1\) 分别需要额外 \(0,1,2\) 次攻击。\(\mathcal{O}(1)\)

1592B. Hemose Shopping *1200

可以证明任何 \(\leq n-x\)\(\geq 1+x\) 的位置都可以被任意重排,故直接找出这些位置排个序看得到的 \(a_i\) 是否递增。\(\mathcal{O}(n\log n)\)

1592C. Bakry and Partitioning *1700

分两种情况讨论:

  • 异或和为 \(0\)。此时删去任意一条边都符合要求。
  • 异或和不为 \(0\)。不妨设其为 \(c\)。此时若 \(k=2\) 则显然无解。否则若树上存在两个不相交子树满足异或和为 \(c\) 则有解(断掉这两个子树,剩下的图异或和也是 \(c\)),可以通过 dfs 求出 \(sz_i\) 表示子树内最多有多少个不相交的异或和为 \(c\) 的 “小子树”,以及 \(x_i\) 表示子树异或和。若 \(sz_i\geq 2\) 或者 \(sz_i=1\)\(x_i=0\)(此时当前子树就可以被分成一个小子树和剩下来的连通块,且异或和都为 \(c\))则有解。否则可以证明无解。

时间复杂度 \(\mathcal{O}(n)\)

const int N=2e5+5;
const int inf=0x3f3f3f3f;

int n,k,a[N];
vector <int> e[N];
int sz[N],xo[N],tot,ok;
void dfs(int id,int f){
	sz[id]=0,xo[id]=a[id];
	for(int it:e[id])
		if(it!=f)dfs(it,id),xo[id]^=xo[it],sz[id]+=sz[it];
	if(sz[id]==0&&xo[id]==tot)sz[id]=1;
	if(sz[id]==1&&xo[id]==0||sz[id]>=2)ok=1;
}
void solve(){
	cin>>n>>k,tot=ok=0;
	for(int i=1;i<=n;i++)cin>>a[i],tot^=a[i],e[i].clear();
	for(int i=1;i<n;i++){
		int u,v; cin>>u>>v;
		e[u].pb(v),e[v].pb(u);
	}
	if(tot==0)return puts("YES"),void();
	if(k==2)return puts("NO"),void();
	dfs(1,0),puts(ok?"YES":"NO");
}
int main(){
	int t=1; cin>>t;
	while(t--)solve();
	return 0;
}

1592D. Hemose in ICPC ? *2300

根据 \(\gcd\) 的性质,至少存在一条树边 \((u,v)\) 使得 \(D(u,v)\) 最大。我们首先询问一整棵树求出 \(r=\max D(u,v)\),然后二分树上一个边集 \(E'\) 大小为 \(\dfrac{m}2\) 的边导出子图,其中 \(m=|E|\) 即当前的决策边集。需要保证整张图尽量连通(满足其导出子图的点两两配对不会查到 \(E\backslash E'\) 中的边),可以 dfs 找。若询问边导出子图内所有点的答案不是 \(r\),说明答案在 \(E\backslash E'\) 中,否则说明答案在 \(E'\) 中。得出结论后缩小决策边集直到其大小为 \(1\) 即答案,最多询问 \(\lceil \log_2(n-1)\rceil+1\leq 11<12\) 次,可以接受。

const int N=1e3+5;
const int inf=0x3f3f3f3f;

int n,mx,lim;
vint e[N];
bool mp[N][N],tg[N][N];
int query(vint q){
	cout<<"? "<<q.size()<<" ";
	for(int it:q)cout<<it<<" ";
	cout<<endl;
	int res; cin>>res;
	return res;
}
void ans(int x,int y){cout<<"! "<<x<<' '<<y<<endl,exit(0);}

vpii cur;
void dfs(int id,int f){
	if(cur.size()>=lim)return;
	for(int it:e[id]){
		if(it==f)continue;
		if(tg[id][it]||!mp[id][it])continue;
		tg[id][it]=tg[it][id]=1;
		cur.pb(id,it),dfs(it,id);
		if(cur.size()>=lim)return;
	}
}

int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v; cin>>u>>v;
		e[u].pb(v),e[v].pb(u),mp[u][v]=mp[v][u]=1;
	}
	
	vint ori;
	for(int i=1;i<=n;i++)ori.pb(i);
	mx=query(ori);
	
	int sz=n-1;
	while(sz>1){
		lim=sz>>1,cur.clear(),ori.clear(),mem(tg,0);
		for(int i=1;i<=n;i++)dfs(i,0);
		static int buc[N]; mem(buc,0);
		for(pii it:cur)buc[it.fi]=buc[it.se]=1;
		for(int i=1;i<=n;i++)if(buc[i])ori.pb(i);
		int res=query(ori);
		if(res==mx){
			for(int i=1;i<=n;i++)
				for(int j=1;j<=n;j++)
					if(!tg[i][j])mp[i][j]=0;
			sz=lim;
		}
		else{
			for(int i=1;i<=n;i++)
				for(int j=1;j<=n;j++)
					if(tg[i][j])mp[i][j]=0;
			sz-=lim;
		}
	}
	if(n==2)ans(1,2);
	else for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				if(mp[i][j])ans(i,j);
	return 0;
}

1592E. Bored Bakry *2400

子区间 \(\rm and>xor\),则其最高位一定相同且长度为偶数:因为长度为奇数时对于剩下来的所有位 \(d\),若子区间第 \(d\) 位全为 \(1\) 则异或和也为 \(1\),不优;而若至少有一个位置第 \(d\) 位为 \(0\) 则该位按位与为 \(0\),不优。

上述结论是充要条件,因为 \(\rm xor\) 最高位为 \(0\)\(\rm and\) 最高位为 \(1\) 故无论低位是什么都符合条件。所以直接枚举最高位就行。时间复杂度 \(\mathcal{O}(n\log a_i)\)

const int N=1e6+5;
const int inf=0x3f3f3f3f;

int n,ans,a[N],pr[N],buc[2][1<<20];
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)a[i]=read(),pr[i]=a[i]^pr[i-1]; 
	for(int i=20;~i;i--){
		mem(buc,-1);
		for(int j=1,r=1;j<=n;r=j=r+1){
			if((a[j]>>i&1)==0)continue;
			while(r<n&&(a[r+1]>>i&1))r++;
			for(int k=j-1;k<=r;k++){
				int res=pr[k]>>i+1;
				if(buc[k&1][res]==-1)buc[k&1][res]=k;
				else ans=max(ans,k-buc[k&1][res]);
			} for(int k=j-1;k<=r;k++)buc[k&1][pr[k]>>i+1]=-1;
		} 
	} cout<<ans<<endl;
	return 0;
}

+*1592F1. Alice and Recoloring 1 *2600

神仙思路题。首先不难发现操作 \(2,3\) 在搞笑,下记操作 \(2\) 为原来的操作 \(4\),记 \(a_{i,j}=[c_{i,j}=\texttt{B}]\)

一次翻转一个子矩形较难处理,考虑对矩形进行一些变换后简化操作,即使一次操作改变的格子尽量少:我们设 \(p_{i,j}\) 表示 \(a_{i,j}\oplus a_{i,j+1}\oplus a_{i+1,j}\oplus a_{i+1,j+1}\)。将 \(a\) 变为全 \(0\) 等价于把 \(p\) 变为全 \(0\)。这一步神仙转换使得对于操作 \(1\),我们相当于翻转 \(p_{x,y}\);对于操作 \(2\),相当于翻转 \(p_{x-1,y-1},p_{x-1,m},p_{n,y-1}\)\(p_{n,m}\)\(x>1,y>1\),因为若 \(x\)\(y=1\) 则可以用两次操作 \(1\) 代替)。

不难发现超过一次进行操作 \(2\) 是不优的,因为这只改变了 \(6\) 个格子的状态(\(p_{n,m}\) 被改变了两次),代价不优于使用六次操作 \(1\)。所以对 \(p\) 求和,若存在 \(x<n,y<m\) 使得 \(p_{x,y}=p_{x,m}=p_{n,y}=p_{n,m}=1\) 则将答案减掉 \(1\)。时间复杂度 \(\mathcal{O}(nm)\)

const int N = 500 + 5;
int n, m, ans, a[N][N], p[N][N];
char mp[N][N];

int main(){
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			cin >> mp[i][j], p[i][j] = mp[i][j] == 'B';
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			ans += a[i][j] = 
				(p[i][j] ^ p[i][j + 1] ^ p[i + 1][j] ^ p[i + 1][j + 1]);
	for(int i = 1, ok = 0; i < n && !ok; i++)
		for(int j = 1; j < m && !ok; j++)
			if(a[i][j] && a[i][m] && a[n][j] && a[n][m])
				ok = 1, ans--;
	cout << ans << endl;
    return 0;
}

+*1592F2. Alice and Recoloring 2

仍然是神仙题。操作 \(2\) 的代价从 \(3\) 减小到了 \(2\)。虽然 F1 操作 \(2\) 最多使用 \(1\) 次的结论不再适用,但我们仍可以发掘一些结论:

observation 1:对于所有操作 \(2\) 的格子 \((x_i,y_i)\),一定满足 \(x_i\) 互不相同且 \(y_i\) 互不相同。证明是容易的,反证说明若存在则只改变 \(4\) 个格子状态不优于四次操作 \(1\)

observation 2:我们只会操作 \(p_{x,y}=p_{x,m}=p_{n,y}=1\) 的格子。这是显然的,因为若上述三个格子至少有一个为 \(0\),则不如花 \(2\) 的代价进行两次操作 \(1\) instead of 花 \(2+1=3\) 的代价进行一次操作 \(2\) 和至少一次操作 \(1\)(根据 observation 1 易得因操作 \(2\)\(0\) 变成 \(1\) 的格子必须用操作 \(1\) 而不是操作 \(2\) 弥补)。

第一个结论给予了我们很清晰的二分图匹配思路,第二个结论说明两部点之间连边的条件。因此根据上述结论建出左部点个数为 \(n-1\),右部点个数为 \(m-1\) 的二分图,跑一遍二分图匹配即可。时间复杂度 \(\mathcal{O}(n^{1.5}m)\)

const int N = 1e3 + 5;
const int M = N * N;

int n, m, T, ans, flow, a[N][N], p[N][N];
char mp[N][N];

int cnt = 1, hd[N], to[M], nxt[M], lim[M];
void add(int u, int v, int w) {
	nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w;
	nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0;
}

int dis[N], cur[N];
bool BFS() {
	queue <int> q;
	mem(dis, 0x3f, N), cpy(cur, hd, N);
	dis[0] = 1, q.push(0);
	while(!q.empty()) {
		int t = q.front(); q.pop();
		if(t == T) return 1;
		for(int i = hd[t]; i; i = nxt[i]) {
			int it = to[i];
			if(lim[i] > 0 && dis[it] > 1e9)
				dis[it] = dis[t] + 1, q.push(it);
		}
	} return 0;
}
int Dinic(int id, int rest) {
	if(id == T || !rest) return rest;
	int flow = 0;
	for(int i = cur[id]; i && rest; i = nxt[i]) {
		cur[id] = i;
		int c = min(rest, lim[i]), it = to[i];
		if(c && dis[id] + 1 == dis[it]) {
			int k = Dinic(it, c);
			if(k == 0) dis[it] = 0;
			lim[i] -= k, lim[i ^ 1] += k;
			rest -= k, flow += k;
		}
	} return flow;
}

int main(){
	cin >> n >> m, T = n + m;
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			cin >> mp[i][j], p[i][j] = mp[i][j] == 'B';
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			a[i][j] = p[i][j] ^ p[i][j + 1] ^ p[i + 1][j] ^ p[i + 1][j + 1];
	for(int i = 1; i < n; i++)
		for(int j = 1; j < m; j++)
			if(a[i][j] && a[i][m] && a[n][j])
				add(i, n + j, 1);
	for(int i = 1; i < n; i++) add(0, i, 1);
	for(int i = 1; i < m; i++) add(n + i, T, 1);
	while(BFS()) flow += Dinic(0, n + m);
    a[n][m] ^= flow & 1;
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++) ans += a[i][j];
	cout << ans - flow << endl;
    return 0;
}

3. Codeforces Round #749 (Div. 1 + Div. 2, based on Technocup 2022 Elimination Round 1)

1586A. Windblume Ode

先求个和,如果是合数直接全选,否则一定是奇数且选择的数中存在至少一个奇数,去掉任意一个即可。

+1586B. Omkar and Heavenly Tree

挺有意思的一道题。注意到 \(m<n\),所以将没有在 \(b_i\) 中出现过的任意一个点 \(p\) 挂上其它所有点形成菊花图即可。不难证明算法正确性。

+*1586C. Omkar and Determination

赛时把我送走的题目。注意到若整张图存在一个点使得其上方和左侧都是 X 则不合法,因为若该点既可能是 . 也可能是 X,无法确定。充分吗?如果该点上方或右方至少有一个 .,那么我们显然可以根据该点的 exitable 情况判断出该点的状态:若为 Y 则只能为 .,而若为 N,由于若该点为 . 则必定可以往左边和上边走,矛盾,故只能为 X

把我送走的原因是我只判断了为 . 的点被判成 N 的情况,而没有注意到题目中 X 若为 N 且上下都为 X 则还可以被判成 .

有了上述结论直接前缀和 + 差分即可,时间复杂度 \(\mathcal{O}(nm+q)\)

const int N = 1e6 + 5;

int n, m, q, sum[N];
string s[N];
int main(){
	cin >> n >> m;
	for(int i = 1; i <= n; i++) cin >> s[i];
	for(int i = 2; i <= n; i++)
		for(int j = 1; j < m; j++)
			sum[j + 1] += s[i][j - 1] == 'X' && s[i - 1][j] == 'X';
	for(int i = 2; i <= m; i++) sum[i] += sum[i - 1];
	cin >> q;
	for(int i = 1; i <= q; i++) {
		int x1, x2; cin >> x1 >> x2;
		cout << (sum[x2] - sum[x1] ? "No" : "Yes") << endl;
	} return 0;
}

+1586D. Omkar and the Meaning of Life

有趣的交互题。

一般遇到这种交互题,如果确定了一个数就很好做了,所以我们尝试通过 \(n\) 个询问固定一个位置的值,再通过 \(n\) 个询问确定整个序列

前者是非常容易的,因为如果你要确定位置 \(i\) 的值,在询问的时候将该位置设为 \(n\),那么 \(p_i+a_i=p_i+n\)。因此从 \(1\)\(n\) 枚举 \(k\),并在其它位置上询问 \(a_j=k\ (j\neq i)\)。若问了 \(n\) 次还没有得到结果,显然 \(p_i=n\):若 \(p_i\neq n\) 那么在询问 \(k=p_i\) 时一定会返回非零值,因为 \(p_i\neq n\) 故排列必定有一个位置 \(j\) 使得 \(p_j=n\),此时 \(p_i+n=n+k\)\(p_i+a_j=p_j+a_j\)。否则,第一次使得到的返回值非零的 \(k\) 就是 \(p_i\)

注意到交互库返回的是位置的最小值,使得如果确定的位置 \(i<n\) 那么较难判断出 \(i\) 后面的数,所以我们就确定 \(p_n\)

现在 \(p_n\) 已经求出,接下来确定整个序列变得轻而易举了:如果我们要知道数 \(i\) 落在哪个位置上,只需询问 \(p_n,p_n,\cdots,p_n,i\),返回值即数字 \(i\) 的下标,正确性显然。

询问刚好 \(2n\) 次,精细实现可以做到 \(2n-2\) 次。CF 上有位老哥给出了 \(n\) 次的做法,比较清晰易懂,这里就不赘述了。

const int N=105;
int n,ans[N];
int main(){
	cin>>n,ans[n]=-1;
	for(int i=1,r;i<=n&&ans[n]==-1;i++){
		cout<<"? ";
		for(int j=1;j<n;j++)cout<<i<<" ";
		cout<<n<<endl;
		cin>>r; if(r)ans[n]=i;
	}
	if(ans[n]==-1)ans[n]=n;
	for(int i=1,r;i<=n;i++){
		cout<<"? ";
		for(int j=1;j<n;j++)cout<<n+1-i<<" ";
		cout<<n+1-ans[n]<<endl;
		cin>>r; if(r)ans[r]=i;
	}
	cout<<"! ";
	for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
	cout<<endl;
	return 0;
}

1586E. Moment of Bloom

无聊题。

考察与每个点相邻的所有边被改变的次数之和的奇偶性,不难发现除了两个端点,路径其它节点的奇偶性都不会改变。

那么就做完了吧,直接对于每个点记录成为端点的次数奇偶性,最后求一下是奇数的端点个数除以 \(2\) 就是答案,因为一条路径最多也一定可以消灭两个成为端点次数是奇数的节点。

否则对原图求任意生成树,输出方案就是端点之间的简单路径,正确性显然(可以根据叶子结点成为端点次数是偶数推断出其与父节点的边重量一定是偶数归纳证明构造出来的方案一定合法)。时间复杂度 \(\mathcal{O}(nq)\)

const int N=3e5+5;

int n,m,q,cnt,vis[N],fl[N],a[N],b[N];
vint e[N],f[N],cur,ans;
void dfs(int id){
	vis[id]=1;
	for(int it:e[id]){
		if(vis[it])continue;
		f[id].pb(it),f[it].pb(id),dfs(it);
	}
}
void dfs2(int id,int fa,int aim){
	cur.pb(id);
	if(id==aim)return ans=cur,void();
	for(int it:f[id]){
		if(it==fa)continue;
		dfs2(it,id,aim);
	} cur.pop_back();
}
int main(){
    cin>>n>>m;
	for(int i=1;i<=m;i++){
		int a,b; cin>>a>>b;
		e[a].pb(b),e[b].pb(a);
	} dfs(1),cin>>q;
	for(int i=1;i<=q;i++)
		cin>>a[i]>>b[i],fl[a[i]]^=1,fl[b[i]]^=1;
	for(int i=1;i<=n;i++)cnt+=fl[i];
	if(cnt==0){
		puts("YES");
		for(int i=1;i<=q;i++){
			cur.clear(),dfs2(a[i],0,b[i]);
			cout<<ans.size()<<endl;
			for(int it:ans)cout<<it<<" ";
			cout<<endl;
		}
	}
	else puts("NO"),cout<<cnt/2<<endl;
	return 0;
}

+*1586F. Defender of Childhood Dreams

神仙题。

一上来猛冲正解是不对的,先考虑 \(k=2\) 怎么做:由于不能出现长度为 \(2\) 的路径,所以我们只能将节点分成前后两半,然后这两部分之间跨块的节点对相互连边。然后呢?不难发现对于两部分内部节点的连边处理变成了两个子问题。由于颜色不能相同,因此子问题与父问题之间连边的颜色不能相同。为了使颜色最少,我们需要把这两部分尽量均分,每次递归减少一半的问题规模,故答案为 \(\lceil\log_2n\rceil\)

想到 \(k=2\) 怎么做,\(k>2\) 就很显然了:将节点按顺序分为 \(k\) 份且跨块的节点对连颜色相同的边,那么颜色相同路径长度最大值显然为 \(k-1\),符合要求,故答案为 \(\lceil \log_kn\rceil\)

怎么证明充分性呢?归纳万岁:\(n=k^{0}=1\) 时,显然答案为 \(c=0\)。假设用 \(c\) 种颜色最多能使有 \(k^c\) 个节点的图满足题意,那么我们选 \(k\) 个点集 \(s_1,s_2,\cdots,s_k\),每个点集符合条件且最多用 \(c\) 个颜色(因为若用了第 \(c+1\) 种颜色,那么跨点集连边时就会出现长度为 \(k\) 且颜色都为 \(k+1\) 的路径,不符合题意),那么点集大小之和,即使用 \(c+1\) 种颜色能够满足题意的图的大小 \(\leq k\times k^c=k^{c+1}\)

时间复杂度 \(\mathcal{O}\left(n^2+\dfrac{n^2}{k}+\dfrac{n^2}{k^2}+\cdots\right)=\mathcal{O}(n)\)

const int N = 1e3 + 5;

int n, k, e[N][N];
int solve(int l, int r) {
	if(l == r) return 0;
	int cnt = 1, len = r - l + 1;
	if(len < k) {
		for(int i = l; i <= r; i++)
			for(int j = i + 1; j <= r; j++)
				e[i][j] = cnt;
		return cnt;
	}
	int p = len / k, rest = len % k;
	vint pos(k + 1, 0), col(n + 1, 0); // 这里开了大小为 n 的数组所以时间复杂度是 n ^ 2 log n, 精细实现即可 n ^ 2
	for(int i = 1; i <= k; i++)
		pos[i] = pos[i - 1] + p + (i <= rest);
	for(int i = 1; i <= k; i++)
		cnt = max(cnt, solve(l + pos[i - 1], l + pos[i] - 1) + 1);
	for(int i = 1; i <= k; i++)
		for(int j = pos[i - 1]; j < pos[i]; j++)
			col[l + j] = i;
	for(int i = l; i <= r; i++)
		for(int j = i + 1; j <= r; j++)
			if(col[i] != col[j])
				e[i][j] = cnt;
	return cnt;
}
int main(){
	cin >> n >> k, cout << solve(1, n) << endl;
	for(int i = 1; i <= n; i++)
		for(int j = i + 1; j <= n; j++)
			cout << e[i][j] << " ";
    return flush(), 0;
}

+**1586G. Omkar and Time Travel

感觉和 1552F Telepanting 很像,但也有很大区别。非常巧妙的一道题目!孩子很喜欢。狂暴膜拜 zky!他的代码比官方题解还要好懂!111

由于进行 time travel 的决策是固定的,因此完成的任务集合 \(s\) 出现的顺序也是固定的。首先考察合法的即可能出现的任务集合 \(s\) 的性质:

  • observation 1:对于两个任务 \([a_i,b_i]\)\([a_j,b_j]\),若 \(a_i<a_j\)\(b_i<b_j\) 且任务 \(j\) 被完成,则任务 \(i\) 一定被完成。为什么呢?考虑在完成任务 \(j\) 前,若任务 \(i\) 没有被完成,那么我们走到 \(b_i\) 的时候一定会返回 \(a_i\) 而不是继续往前走到 \(b_j\),故该情况不合法。此外,若任务 \(i\) 被任务 \(k\) 撤销,则有 \(a_k<a_i<a_j\) 即任务 \(j\) 也被撤销。因此,对于一个固定的任务集合 \(s\) 要求满足其中的任务都被完成,若存在 \(i,j\in s, a_i<a_j\and b_i<b_j\),则任务 \(i\) 可以被忽略。上述性质使得所有任务集合 \(s\) 在经过 “除杂” 后一定是一环套一环的形态,即 \(a_1<a_2<\cdots<a_n<b_n<\cdots<b_2<b_1\)禁 止 套 娃

因为完成的任务集合出现顺序固定,所以它们之间一定有偏序关系。为了求出题目的答案,可以考虑对于两个任务集合 \(s_x,s_y\),如何判断哪一个先出现:

  • observation 2:将集合按照右端点从小到大排序,除去相同的任务后缀之后,右端点更大的那个任务集合先出现。因为如果 Omkar 现在在时刻 \(b_i\) 处且任务 \(i\) 已经被完成(即不需要再回到时间 \(a_i\)),则 \(b_j<b_i\) 的所有任务 \(j\) 也一定被完成。根据 observation 1 易证。

上述两个结论对解题至关重要,我们已经可以大概给出一个算法框架了:考虑枚举已经完成的任务集合 \(s\) 进行除杂后套在最外层的一个任务 \([a_i,b_i]\),因为这样保证了不会重复计数,也不会漏记,因为每个可能出现任务与其除杂后的结果是一一对应的,即一个任务集合唯一对应一个除杂后的任务集合,且一个除杂后的任务集合唯一对应一个实际上的任务集合(可以反推回去)。

很显然,对于枚举的 \([a_i,b_i]\),任何 \(a_j<a_i\)\(b_j<b_i\)\(j\) 都已经被完成,故我们只需考虑被套在内层的任务的完成情况方案数。这是一个子问题,因此有一个很明显的动态规划模型:设 \(f_i\) 表示套在 \([a_i,b_i]\) 里面的任务完成情况的方案数。转移不可能出现环,因为两个区间不可能相互包含。

具体怎么做这个 DP 呢,可以将区间按照 \(a\) 从小到大排序,然后从后往前 DP(显然不可能出现前面的区间转移到后面的情况)。注意到排过序后对于任意 \(i<j\),若 \(b_j<b_i\)\(i\) “套住” 了 \(j\),所以考虑把 \(j\) 的 DP 值 \(f_j\) 挂在 \(b_j\) 处,那么 \(f_i\) 就是对区间 \([a_i,b_i]\) 求和再 \(+1\)(因为可以内层啥都没有),可以用 BIT 维护。


统计答案也很有难度。根据 observation 2,对于每个任务 \(i\),若 \(b_i< \max_{j\in s}b_j\) 则以 \(i\) 为除杂后最外层任务的任务集合一定在 \(s\) 前出现。但真的只有这些吗?不妨设 \(s\) 中取到右端点最大值的任务为 \(p\)。那些除杂后最外层是 \(p\) 的任务集合我们还没有计算到。考虑剥掉 \(p\) 再做一遍(注意还要考虑到除杂后只有单独一个 \(p\) 的任务集合,故每计算一个 \(p\) 都要将答案加上 \(1\)),设新的 \(p\)\(p'\),这个时候 \(i\) 就得满足 \(a_p<a_i\)\(b_i<b_{p'}<b_p\),不断这样递归下去。这样统计答案的复杂度是 \(\mathcal{O}(n^2)\) 的,无法接受。

实际上,对于每个 \(p\),将上述被计入答案的任务分为两部分:一部分是 \(a_i<a_p\)\(\sum f_i\),另一部分是 \(a_i>a_p\)(此时 \(i\)\(p\) 完全包含)的 \(\sum f_i\) 加上除杂后只剩单独一个 \(p\) 的任务集合的 \(1\),注意到前者的答案在递归统计 \(p’\) 的答案时不会被计入(只会被算一次),而后者的答案恰好就是 \(f_p\)!这就很舒服了,稍作转化,答案即 \(\sum f_i[\exist j\in s, a_i\leq a_j\land b_i\leq b_j]\),再用一个 BIT 维护即可。时间复杂度 \(\mathcal{O}(n\log n)\)

const int N = 4e5 + 5;
const int mod = 1e9 + 7;
void add(int &x, int y) {x += y; if(x >= mod) x -= mod;}

int n, ans, t, f[N], buc[N];
struct Interval {
	int l, r, id;
	bool operator < (const Interval &v) const {
		return l < v.l;
	}
} s[N];

struct Fenwick {
	int c[N];
	void Inc(int x, int v) {while(x <= n << 1) add(c[x], v), x += x & -x;}
	int Que(int x) {int s = 0; while(x) add(s, c[x]), x -= x & -x; return s;}
} fen, ap;

int main(){
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> s[i].l >> s[i].r, s[i].id = i;
	sort(s + 1, s + n + 1), cin >> t;
	for(int i = 1, id; i <= t; i++) cin >> id, buc[id] = 1;
	for(int i = n; i; i--) {
		fen.Inc(s[i].r, f[i] = (1 + fen.Que(s[i].r)) % mod);
		if(buc[s[i].id]) ap.Inc(s[i].r, 1);
		if(ap.Que(n << 1) - ap.Que(s[i].r - 1)) add(ans, f[i]);
	} cout << ans << endl;
    return 0;
}

4. Codeforces Round #750 (Div. 2)

1582A. Luntik and Concerts

注意到 \(a,b,c\) 下界均为 \(1\),因此可以这样构造:首先依次考虑 \(a\)\(1\)\(c\)\(3\),哪个音乐会时间更少就放到里面,若相等则任意放。我们就得到了一个两个时长相差不超过 \(3\) 的演唱会,且相差是奇数还是偶数由 \(a+3c\)\(a+c\) 的奇偶性决定。由于至少有一个 \(2\) 因此我们一定能将时长差从 \(3\) 缩减到 \(1\)

对于剩下的 \(2\),若相差为 \(1\) 则放到时长较少的那个音乐会里面从而保证时长差为 \(1\)。相差为 \(2\) 同理。若相差为 \(0\),那么我们将有 \(1\) 的那个音乐会的 \(1\) 挪到另一个里面去,再放上一个 \(2\) 即可保证相差恒为 \(0\)

综上,我们只需输出 \(a+c\)\(2\) 取模后的结果即可。时间复杂度 \(\mathcal{O}(T)\)

int main() {
	int T, a, b, c; cin >> T;
	while(T--) cin >> a >> b >> c, cout << (a + c & 1) << endl;
	return 0;
}

1582B. Luntik and Subsequences

乘法原理和加法原理的简单应用。

对于每个 \(0\),我们可以选择选或不选,因为不影响选出的数的和。对于所有 \(1\),我们最多也必须选择 \(1\) 个才能符合题意。选择其它数会让和减小至少 \(2\),不符合题意。

因此,令 \(1\) 的个数为 \(a\)\(0\) 的个数为 \(b\),则答案为 \(a\ \text{(有 a 种方法选 1)}\times 2^b\ \text{(对于每个 0 有 2 种方法,共有 b 个 0)}\)。时间复杂度 \(\mathcal{O}(n)\)。需要开 long long。

int main() {
	int T, n, v; cin >> T;
	while(T--) {
		ll a = 0, b = 0; cin >> n;
		while(n--) {
			cin >> v;
			if(v == 1) a++;
			else if(!v) b++;
		} cout << (a << b) << endl;
	}
	return 0;
}

1582C. Grandma Capa Knits a Scarf

考虑枚举被删去的字符然后维护头尾指针贪心,时间复杂度 \(\mathcal{O}(n\Sigma)\)。可以通过找到第一个不合法的地方做到 \(\mathcal{O}(n)\)

1582D. Vupsen, Pupsen and 0

对于两个数 \(a,b\),只要令它们前面的系数为 \(x=b\)\(y=-a\) 就能满足 \(xb+yb=ab-ab=0\)。若 \(n\) 为奇数还需考虑三个数 \(a,b,c\) 的情况。此时我们必然能找到两个数使得它们和不为 \(0\),不妨设为 \(b,c\),那么令 \(x=b+c\)\(y=-a\)\(z=-a\) 即可做到 \(xa+yb+zc=(b+c)a-ab-ac=0\)。注意虽然 \(|x|\) 可能 \(>10^4\)\(n<10^5\)\(|x|\leq 2\times 10^4\) 因此仍然有绝对值总和 \(\leq 10^9\)

const int N = 1e5 + 5;
int T, n, a[N];

int main() {
	cin >> T;
	while(T--) {
		int bg = 1; cin >> n;
		for(int i = 1; i <= n; i++) cin >> a[i];
		if(n & 1) {
			if(a[2] + a[3]) cout << a[2] + a[3] << " " << -a[1] << " " << -a[1] << " ";
			else if(a[1] + a[2]) cout << -a[3] << " " << -a[3] << " " << a[1] + a[2] << ' ';
			else cout << -a[2] << " " << a[1] + a[3] << " " << -a[2] << " ";
			bg = 4;
		} for(int i = bg; i <= n; i += 2) cout << a[i] << " " << -a[i + 1] << " ";
		cout << endl;
	}
	return 0;
}

如果限制了总和 \(\leq n\times 10^4\) 怎么办?

对于三个数 \(a,b,c\) 的情况,我们令 \(x=b\),此时变为 \((a+y)b+zc=0\) 即两个数的情况。令 \(y=c-a,z=-b\)。但此时可能 \(|c-a|>10^4\),没关系,对 \(x\) 取个反变为 \(-b\),那么 \(y\) 就变成了 \(c+a\),显然 \(|c+a|\leq 10^4\)\(|c-a|\leq 10^4\) 至少满足一个,故合法。

还有一些情况需要特判:\(c=a\)\(c=-a\),这是 trival 的,因为可以看成两个数来做。综上,我们保证了 \(\sum |x_i|\leq n\times 10^4\)(一些细节优化之后可以做到 \(|x_i|\leq 10^4\)),是一个更强的算法。以下是赛时代码。

void solve(int a,int b){cout<<b<<" "<<-a<<" ";}
void solve(int a,int b,int c){
	if(a==b&&b==c)cout<<"1 1 -2 ";
	else if(a==c){
		int coef=-b,x,y;
		if(coef<0)x=1,y=coef-1;
		else x=-1,y=coef+1;
		cout<<x<<" "<<a<<" "<<y<<" ";
	} else if(a==-c){
		int coef=b,x,y;
		if(coef<0)y=-1,x=coef-1;
		else y=1,x=coef+1;
		cout<<x<<" "<<-a<<' '<<y<<" ";
	}
	else if(abs(c-a)<=1e4)
		cout<<b<<" "<<c-a<<" "<<-b<<" ";
	else cout<<-b<<' '<<c+a<<" "<<-b<<" ";
}
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	if(n%2==0)for(int i=1;i<=n;i+=2)solve(a[i],a[i+1]);
	else{
		solve(a[1],a[2],a[3]);
		for(int i=4;i<=n;i+=2)solve(a[i],a[i+1]);
	}
	cout<<endl;
}

1582E. Pchelyonok and Segments

简单 DP。如果从前往后考虑,我们不知道第一个区间长度是多少,因此从后往前 DP。设 \(f_{i,j}\) 表示计算到第 \(i\) 位的后缀,存在一个满足条件且最长区间长度 \([l,r]\)\(j\)\([l,r]\) 区间和的最大值。

转移首先 \(f_{i,j}\gets f_{i+1,j}\),然后计算 \([i,i+j-1]\) 的区间和 \(s\),若 \(s<f_{i+j,j-1}\) 则可以选择 \([i,i+j-1]\),令 \(f_{i,j}\gets \max(f_{i,j},s)\)

由于区间最长不超过 \(\sqrt n\) 故时间复杂度为 \(\mathcal{O}(n\sqrt n)\)。赛时因为没开 long long 挂了一发 /yun。

+1582F2. Korney Korneevich and XOR (hard version)

看到题目一个基本的想法是设 \(f_{i,j}\) 表示是否存在以前 \(i\) 位以 \(j\) 结尾且异或和为 \(k\) 的子序列,这样做复杂度爆炸,是 \(nV^2\) 的(\(V=2^{13}-1\geq \max a_i\))。

实际上从前往后考虑的时候我们并不关心当前位置,因此实际有用的状态只有 \(V^2\) 个:以哪个数结尾,以及异或和是多少。

但是这个状态设计有些鸡肋,我们没办法优化。换个 DP 方法:设 \(f_{j,k}\) 表示是否存在以 \(<j\) 的数结尾且异或和为 \(k\) 的子序列。假设当前数为 \(a_i\),那么枚举 \(k\),若 \(f_{a_i,k}=1\) 则更新 \(f_{j,k\oplus a_i}\ (a_i<j\leq V)\)。实际上对于相同的 \(k\oplus a_i\) 我们并没有必要每次都从 \(a_i\) 枚举到 \(V\) 来更新,因为我们知道若 \(f_{j,k}\) 被更新为 \(1\),所以任何 \(>j\)\(j’\)\(f_{j',k}\) 也一定是 \(1\),不需要继续增大下去了。因此,记录一个 \(mx_v\) 表示下一次遇到 \(k\oplus a_i=v\) 的时候应该从 \(a_i+1\) 枚举到 \(mx_v\),然后用 \(a_i\) 更新 \(mx_v\) 即可。

此外,对于同一个 \(a_i\)\(f_{a_i,k}\),若 \(k\) 枚举过了就没有必要再枚举。故开一个桶 \(buc_{a_i}\) 记录 \(f_{a_i,k}\) 还没有被枚举过的 \(k\) 有哪些,遇到 \(a_i\) 就全部更新掉 \(k\oplus a_i\) 然后把 \(buc_{a_i}\) 清空。

复杂度分析:枚举复杂度:每个 \(buc_{a_i}\) 最多存在过 \(V\) 个数,共有 \(a_i\) 个这样的桶。更新复杂度:每个 \(v\)\(mx_v\) 会从 \(V\) 枚举到 \(0\),共有 \(V\) 个这样的 \(v\)。因此时间复杂度为 \(\mathcal{O}(n+V^2)\)

const int V = 1 << 13;
int n, ans = 1, vis[V], mx[V];
vint buc[V];

int main() {
	cin >> n, vis[0] = 1;
	for(int i = 1; i < V; i++) buc[i].pb(0), mx[i] = V - 1;
	for(int i = 1; i <= n; i++) {
		int a = read();
		for(int it : buc[a]) {
			int p = it ^ a; ans += !vis[p], vis[p] = 1;
			while(mx[p] > a) buc[mx[p]--].pb(p); 
		} buc[a] = {};
	} cout << ans << endl;
	for(int i = 0; i < V; i++)
		if(vis[i]) cout << i << " ";
	cout << endl;
	return 0;
}

+*1582G. Kuzya and Homework

对于每个位置 \(i\) 我们求出 \(pre_i\) 表示如果要让以 \(i\) 结尾的区间是好区间则左端点至少要在 \(pre_i\) 及其左边。若 \(s_i=\texttt{*}\) 显然 \(pre_i=i\),否则 \(s_i=\texttt{/}\),可以这么做:

实际上这个 “不出现分数” 的性质和括号匹配十分类似:对于每个质数 \(p\),若把乘看做左括号,除看做右括号,那么不出现 \(\dfrac 1 p\) 等价于不存在没有被匹配的右括号。这启发我们对每个质数用一个栈维护其 “括号序列”,若是乘则加入左括号,否则弹出左括号。

对于除法,最后一次被弹出的括号的位置就是 \(pre_i\),因为若 \(j>pre_i\) 那么 \(s_i\) 处的右括号没法被匹配。相反,如果已经没有左括号可以弹了,那么显然任何以 \(i\) 结尾的区间都不是好区间,令 \(pre_i=0\)。 注意一个位置可能贡献多个左括号或右括号

不难发现若 \(l=\min_{i=l}^r pre_i\)\([l,r]\) 是好区间。考虑从后往前用单调栈维护从从栈底到栈顶从小到大的 \(pre_i\),若当前 \(pre_i=i\) 则加上栈顶矩形宽度即 \(p-i+1\),其中 \(p\) 最大且满足对于任意 \(q\in[i,p]\) 都有 \(pre_q\geq i\),这是单调栈的过程中顺带维护的信息,可以类比求图中最大矩形的问题。当然,线段树二分维护最小值也可以。

前者若一个位置加入多个左右括号时捆绑在一起看是 \(\mathcal{O}(n\omega(a_i))\),拆开来看是 \(\mathcal{O}(n\log a_i)\),后者时间复杂度 \(\mathcal{O}(n\log a_i)\)

const int N = 1e6 + 5;
int vis[N], mp[N], pr[N], cnt;
void sieve() {
	for(int i = 2; i < N; i++) {
		if(!vis[i]) mp[i] = pr[++cnt] = i;
		for(int j = 1; j <= cnt && i * pr[j] < N; j++) {
			vis[i * pr[j]] = 1, mp[i * pr[j]] = pr[j];
			if(i % pr[j] == 0) break;
		}
	}
}

int n, pre[N], a[N];
vint buc[N];

int top, stc[N], w[N];
ll ans;

bool Med;
int main() {
	cin >> n, sieve();
	for(int i = 1; i <= n; i++) a[i] = read();
	for(int i = 1; i <= n; i++) {
		char s = gc;
		while(s != '*' && s != '/') s = gc;
		if(s == '*') {
			while(a[i] > 1) {
				int p = mp[a[i]];
				buc[p].pb(i), a[i] /= p;
			} pre[i] = i;
		} else {
			pre[i] = i;
			while(a[i] > 1) {
				int p = mp[a[i]];
				if(buc[p].empty()) pre[i] = -1;
				else cmin(pre[i], buc[p].back()), buc[p].pop_back();
				a[i] /= p;
			}
		}
	}
	for(int i = n; i; i--) {
		int nw = 1;
		while(top && pre[i] <= stc[top]) nw += w[top], top--;
		stc[++top] = pre[i], w[top] = nw;
		if(pre[i] == i) ans += nw;
	} cout << ans << endl;
	return 0;
}
posted @ 2021-10-20 09:09  qAlex_Weiq  阅读(74)  评论(0)    收藏  举报