MX 暑假集训 7.30

【初音ミク】画脂镂冰【かんてゐく】

这个真的很好听 \(\uparrow\) /xin。

贪心专题(为什么贪心专题里会放这么多 dp,mx 你无敌了)。

Another MEX Problem

题意

给定一个长度为 \(n\) 的序列,你需要选出若干个不交子数组使每个子数组的 \(\operatorname{mex}\) 的异或和最大。

\(1\le n\le 5000\)

solution

考虑由于子数组是不交的,那么我们用序列的一个前缀 \([1,r]\) 选出若干个子数组得到一个异或和 \(x\),那么肯定希望 \(r\) 尽可能小,这样后面的操作空间更大。

异或和显然不会大于 \(2n\),所以令 \(f_i\) 表示最小的 \(r\) 使得 \([1,r]\) 能选出若干个子数组,其 \(\operatorname{mex}\) 的异或和为 \(i\)

\(mx_{l,x}\) 表示最小的 \(r\) 满足 \([l,r]\) 中可以选出一个子数组使得这个子数组的 \(\operatorname{mex}\)\(x\),这个显然可以 \(n^2\) 预处理出来。

\(f\) 转移有:\(f_{i\oplus j}\leftarrow \min(f_{i\oplus j},mx_{f_i+1,j})\),转移类似于最短路,跑 dijkstra 即可。

时空复杂度 \(O(n^2)\)

Code
#include<cstdio>
#include<algorithm>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=5e3+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
int a[N],f[N<<1];
short mx[N][N];
bitset<N<<1> vis,did;

inline void solve() {
	read(n);
	f(i,1,n) read(a[i]);
	f(i,1,n<<1) f[i]=inf;
	f(i,1,n+1) f(j,0,n+1) mx[i][j]=n+1;
	f(l,1,n) {
		int mex=0;
		f(r,l,n) {
			vis.set(a[r]);
			while (vis[mex]) mex++;
			if (!did[mex]) {
				did.set(mex);
				mx[l][mex]=r;
			}
		}
		f(i,0,n) vis.reset(i),did.reset(i);
	}
	f(i,0,n) F(j,n-1,1) mx[j][i]=min(mx[j+1][i],mx[j][i]);
	int ans=0;
	while (1) {
		int id=-1,mxv=inf;
		f(i,0,n<<1) if (!vis[i]&&f[i]<mxv) id=i,mxv=f[i];
		if (id==-1) break;
		int l=f[id]+1;
		f(now,0,n) if (mx[l][now]<=n) f[id^now]=min(f[id^now],(int)mx[l][now]);
		vis.set(id);
		ans=max(ans,id);
	}
	printf("%d\n",ans);
	f(i,0,(n<<1)) vis.reset(i);
	f(i,1,n+1) f(j,0,n+1) mx[i][j]=0;
}

int main() {
	int T;
	read(T);
	while (T--) solve();
	return 0;
}

Poor Turkeys

题意

\(n\) 只鸡,有 \(m\) 个人,每个人有参数 \(x_i,y_i\),他们依次进行以下操作:

  • 若第 \(x_i\) 只鸡和第 \(y_i\) 只鸡都存在,那么随机选择一只拿走;
  • 若第 \(x_i\) 只鸡和第 \(y_i\) 只鸡只有一只存在,那么将存在那只拿走;
  • 若第 \(x_i\) 只鸡和第 \(y_i\) 只鸡都不存在,那么他不会干任何事。

问有多少对二元组 \((i,j)(i<j)\),满足第 \(i\) 只鸡和第 \(j\) 只鸡可能在所有操作后仍然不被取走。

\(1\le n\le 400,1\le m\le 10^5\)

solution

简单暴力题,以下用存活表示不被取走。

枚举第 \(i\) 只鸡,考虑如何判断第 \(i\) 只鸡存活的条件,考虑倒序考虑每个人,维护集合 \(S\) 表示当前需要保证 \(S\) 内所有鸡都存活,最终状态(也就是倒序枚举的开始)要求 \(i\in S\),那么对于一个人 \((x,y)\),分以下情况讨论:

  • \(x\not\in S\land y\not\in S\),那么 \(x,y\) 选哪只都不影响 \(i\) 是否存活,不考虑;
  • \(x\in S \land y\in S\),那么无论取哪只鸡都会导致 \(i\) 无法存活,故 \(i\) 必然被取走;
  • 否则钦定 \(x\in S\land y\not\in S\),那么将 \(y\) 加入集合 \(S\)(因为要保证 \(x\) 不被取走,则这个人必须取走 \(y\),就要保证之前 \(y\) 不被取走)。

现在得到了 \(n\) 个集合,令第 \(i\) 只鸡存活需要的集合为 \(S_i\),那么对于一对 \((i,j)\)(满足 \(i,j\) 均不必然被取走),若 \(S_i\cap S_j=\emptyset\),说明这两只鸡存活的条件独立,可以同时存活,否则 \(S_i\cap S_j\ne \emptyset\),说明一定存在一只鸡 \(k\),使得存在两个不同时刻,一个时刻 \(i\) 需要存活时需要将 \(k\) 取走,另一个时刻 \(j\) 需要存活时需要将 \(k\) 取走,而 \(k\) 只能被取走一次,不满足条件,所以二元组 \((i,j)\) 合法等价于 \(S_i\cap S_j=\emptyset\)

求出每个 \(S\) 后暴力枚举 \((i,j)\) 判断 \(S_i\)\(S_j\) 是否有交即可,时间复杂度 \(O(nm+\frac{n^3}{\omega })\)

Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=400+10;
constexpr int M=1e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m,ans;
int u[M],v[M];
bitset<N> tmp[N],del;

int main() {
	read(n);read(m);
	f(i,1,m) read(u[i]),read(v[i]);
	f(i,1,n) {
		tmp[i].set(i);
		F(j,m,1) {
			int x=u[j],y=v[j];
			if (tmp[i][x]&&tmp[i][y]) {
				del.set(i);
				break;
			}
			if (tmp[i][x]) tmp[i].set(y);
			if (tmp[i][y]) tmp[i].set(x);
		}
	}
	f(i,1,n) {
		if (del[i]) continue;
		f(j,i+1,n) if (!del[j]) ans+=(!(tmp[i]&tmp[j]).any());
	}
	printf("%d\n",ans);
	return 0;
}

[ZJOI2020] 序列

题意

给定一个长度为 \(n\) 的非负整数序列,下标从左到右依次为 \(1\)\(n\),你可以进行以下几种操作:

  • 选定 \(l,r\),将下标在 \([l,r]\) 内的数减去 \(1\)
  • 选定 \(l,r\),将下标为奇数且在 \([l,r]\) 内的数减去 \(1\)
  • 选定 \(l,r\),将下标为偶数且在 \([l,r]\) 内的数减去 \(1\)

求至少多少次操作后使整个序列全部变成 \(0\)

\(1\le T\le 5,1\le n\le 10^5\)

solution

为啥我觉得这个很简单,贪一下就完了。

称第一种(整个区间减)操作为直线,另外两种为折线。

从左到右扫描每个位置,维护能够向右延伸的直线和折线的当前数量。

先发现几个性质:

  • 对于两个相邻位置,两个位置同时作为两条折线的左端点一定不优,因为将两条奇偶性不同的折线换成一条直线显然不劣;

  • 对于相邻两个数 \(x,y\),不考虑前面是否有直线或折线可以继承,那么最优情况肯定是从 \(x\) 发出 \(\min(x,y)\) 条直线,若 \(x>y\) 则多余的部分由 \(x\) 发出 \(x-y\) 条折线,使其覆盖 \(x\) 而不覆盖 \(y\),否则继续考虑 \(y\)\(y\) 的下一个数,因为由第一条性质可知两个位置不可能同时发出折线,那么一个位置发出直线将这两个位置尽可能多的削减成 \(0\),若前面的位置还有剩余元素,则折线可能可以将后面的元素进行削减,一定不劣于直线;

  • 对于每一个位置,会尽可能多地继承前面的线(无论是直线还是折线),因为如果放弃前面可继承的线而自己发出线一定不优于继承前面的线同时让后面的位置发出线,比如位置 \(i\) 发出折线,不会优于位置 \(i\) 继承前面的线,然后 \(i+2\) 发出折线;

根据这几条性质我们可以大致确定一个贪心的思路:

当前考虑到 \(i\),令 \(c_0\) 为前面直线的数量,\(c_1\) 为前面折线(与 \(i\) 奇偶性相同)的数量,\(c_2\) 为前面折线(与 \(i\) 奇偶性不同)的数量。

根据第二条性质,结合 \(c_2\) 对下一个位置的削减,令 \(x=\min(a_i,\max(0,a_{i+1}-c2)),y=a_i-x\),则第 \(i\) 个位置最优方案是在 \(i\) 后还剩下 \(x\) 条直线和 \(y\) 条折线,又因为第三条性质,我们会尽可能接受前面的线,所以我们可以在接受前面所有线的同时,尽可能让直线数量接近 \(x\),折线数量接近 \(y\),若传入的线数量大于 \(a_i\),那么就尽可能地让传入的直线与需要传出的 \(x\) 条直线尽可能多地匹配,传入折线与需要传出的 \(y\) 条折线尽可能多地匹配,匹配完后可能会出现传出 \(x\) 不足或传出 \(y\) 不足的情况,根据第三条性质,我们只能让多的一些直线或折线传出而不是从 \(i\) 重新开一条线补足 \(x\)\(y\);若传入线的数量不足 \(a_i\),则在接受所有线的同时,需要发出一些线,只需要像上面那样使发出的直线数量尽可能接近 \(x\),发出折线的数量尽可能接近 \(y\) 即可,结合代码可能更清晰一些。

时间复杂度 \(O(Tn)\)

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
int a[N];

inline void solve() {
	read(n);
	f(i,1,n) read(a[i]);a[n+1]=inf;
	ll ans=0,c0=0,c1=0,c2=0;
	f(i,1,n) {
		int x=min((ll)a[i],max(0ll,a[i+1]-c2)),y=a[i]-x;
		if (c0+c1>=a[i]) {
			c0=min(c0,(ll)a[i]);
			c1=min(c1,(ll)a[i]);
			if (c0>=x&&c1>=y) c0=x,c1=y;
			else if (c0<x) c1=a[i]-c0;
			else c0=a[i]-c1;
		}
		else {
			ans+=(a[i]-c0-c1);
			if (c0<x&&c1<y) c0=x,c1=y;
			else if (c0<x) c0=a[i]-c1;
			else c1=a[i]-c0;
		}
		swap(c1,c2);
	}
	printf("%lld\n",ans);
}

int main() {
	int T;
	read(T);
	while (T--) solve();
	return 0;
}

Points Movement

题意

给定 \(n\) 个点和 \(m\) 条线段,若一个点在一条线段上,则可以将这条线段标记为访问过的线段,你可以花 \(1\) 的代价将任意一个点向左或向右移动一个距离,求将所有线段访问的最小代价。

\(1\le n,m\le 2\times 10^5\)

solution

气笑了,和贪心没有任何关系。

首先若一条线段包含另一条线段,则将被包含的线段保留,包含它的线段去掉,再将所有点不移动就能访问的线段去掉,于是剩余的线段满足在两个点之间,且随左端点增加右端点也单调递增。

\(i\) 个点需要访问第 \(i-1\) 个点到第 \(i\) 个点之间的一个后缀的线段,和第 \(i\) 个点到第 \(i+1\) 个点之间的一个前缀的线段,若访问这些线段需要向左走 \(x\) 的距离,向右走 \(y\) 的距离,那么先向左走代价就是 \(2x+y\),先向右走代价就是 \(x+2y\),区别在于给 \(x\) 还是给 \(y\) 附上一个 \(2\) 的系数。

考虑 dp,令 \(f_{i,0/1}\) 表示已经访问完第 \(i\) 个点以前的所有线段,其中第 \(i\) 个点先向左或先向右走的最小代价,对于第 \(i\) 个点与第 \(i+1\) 个点之间的转移,枚举中间线段的分界点,分界点左边贡献由 \(i\) 计入,右边贡献由 \(i+1\) 计入,根据第二位的状态考虑是否需要乘上 \(2\) 的系数即可转移,由于总线段数量为 \(m\),所以转移时间复杂度为 \(O(n+m)\)

时间复杂度 \(O(n\log n+m\log m)\),瓶颈在排序。

Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;
constexpr ll INF=1e18+10;

inline void read(int &x) {
	x=0;int f=1;
	char ch=getchar();
	while (ch<48) {if(ch=='-')f=-1;ch=getchar();} 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();x*=f;
}

int n,m;
int a[N],lst[N];
bitset<N> del;
ll dp[N][2];

struct Node{int l,r;}p[N];
vector<Node> tmp[N];
inline bool cmp(Node x,Node y) {return (x.l==y.l)?(x.r<y.r):(x.l<y.l);}

inline void work() {
	sort(a+1,a+1+n);
	sort(p+1,p+1+m,cmp);
	static int st[N];
	int now=0,top=0;
	f(i,1,m) {
		while (now<n&&a[now+1]<p[i].l) now++;
		if (now<n&&p[i].l<=a[now+1]&&a[now+1]<=p[i].r) {
			del.set(i);
			continue;
		}
		while (top&&p[st[top]].r>=p[i].r) del.set(st[top--]);
		st[++top]=i;
	}
}

inline void solve() {
	read(n);read(m);
	f(i,1,n) read(a[i]);a[0]=-inf;
	f(i,1,m) read(p[i].l),read(p[i].r);
	work();
	int now=0;
	f(i,1,m) if (!del[i]) {
		while (now<n&&a[now+1]<=p[i].l) now++;
		tmp[now].push_back(p[i]);
	}
	f(i,1,n) dp[i][0]=dp[i][1]=INF;
	int mnL=a[1],mxR=a[n];
	for (Node k:tmp[0]) mnL=min(mnL,k.r);
	for (Node k:tmp[n]) mxR=max(mxR,k.l);
	dp[1][0]=a[1]-mnL;
	dp[1][1]=(a[1]-mnL)*2ll;
	f(i,1,n-1) {
		if (tmp[i].empty()) {
			dp[i+1][0]=dp[i+1][1]=min(dp[i][0],dp[i][1]);
			continue;
		}
		lst[tmp[i].size()]=a[i+1];
		for (int j=(int)(tmp[i].size()-1);j>=0;j--) lst[j]=tmp[i][j].r;
		dp[i+1][0]=dp[i][0]+(a[i+1]-lst[0]);
		dp[i+1][1]=dp[i][0]+(a[i+1]-lst[0])*2ll;
		for (int j=0,mxrg=tmp[i].size();j<mxrg;j++) {
			ll p1=tmp[i][j].l,p2=lst[j+1],v1=(a[i+1]-p2),v2=v1*2,v3=(p1-a[i]),v4=v3*2;
			dp[i+1][0]=min({dp[i+1][0],dp[i][0]+v4+v1,dp[i][1]+v3+v1});
			dp[i+1][1]=min({dp[i+1][1],dp[i][0]+v4+v2,dp[i][1]+v3+v2});
		}
	}
	printf("%lld\n",min(dp[n][0]+(mxR-a[n])*2ll,dp[n][1]+(mxR-a[n])));
	f(i,0,n) tmp[i].clear();
	f(i,1,m) del.reset(i);
}

int main() {
	int T;
	read(T);
	while (T--) solve();
	return 0;
}

Camel and Oases

题意

\(n\) 个绿洲沿着数轴排布,第 \(i\) 个绿洲在 \(a_i\) 处。

骆驼想要访问所有绿洲(顺序任意,可以多次访同一个绿洲),初始时它有储水量 \(V\),到达每个绿洲时,它可以进行储水,获得等同于当前储水量体积的水,每个绿洲可以进行无限次储水,他有以下两种移动方式可以任意选择,令当前储水量为 \(v\)

  • 移动到距离当前坐标 \(d\) 个单位距离的绿洲,消耗 \(d\) 的水量,水量不能为负;
  • 跳跃到坐标轴上任意一个位置,要求当前水量不为 \(0\),跳跃后 $v\leftarrow \left\lfloor \frac{v}{2} \right\rfloor $,并且水量归零。

初始时骆驼位于第 \(i\) 个绿洲,询问它是否能够访问所有绿洲,对于 \(i=1,2,\dots ,n\) 分别求解。

\(1\le n,V\le 2\times 10^5\)

solution

不算难但是挺有意思的题。

首先对于当前储水量 \(v\),骆驼肯定要将所有只通过移动能访问的绿洲都访问一次,每到达一个绿洲一定会进行储水,于是一个 \(v\),这些绿洲一定会分成若干个段,使得段内绿洲两两可达。

发现有意义的储水体积 \(v\) 仅有 \(\log V\) 个,我们可以考虑对于每个体积,求出绿洲形成段的状态,那么如何判定从一个绿洲出发,是否能到达所有绿洲呢?

能到达所有绿洲等价于在所有储水量层中选出一个段,使得所有段覆盖了所有绿洲至少一次。

考虑 dp,令 \(dpR_s\) 表示已经将 \(s\) 中的所有层选出了一个段,最多能覆盖的前缀,\(dpL_s\) 表示已经将 \(s\) 中的所有层选出了一个段,最多能覆盖的后缀,这里的 \(s\) 是不考虑储水量为 \(V\) 的层的,因为我们一开始要钦定在这个层的某一段内。

转移很简单,以 \(dpR\) 的转移为例,令 \(id_{i,j}\) 表示第 \(i\) 层位置 \(j\) 的段编号,\(R_{i,j}\) 表示第 \(i\) 层第 \(j\) 段的右端点,那么有以下转移:

\[dpR_s\leftarrow \max\limits_{i\in s}(R_{i,id_{i,dp_{s\setminus i}+1}}) \]

\(dpL\) 同理,对于每个 \(i\),我们可以枚举集合 \(s\),让 \(s\) 内的所有层组成前缀,其他层组成后缀,前后缀的并再并上初始储水量的 \(i\) 所属块是否覆盖了全集,但是这样枚举是 \(O(nV)\) 的,不可接受。

注意到若初始储水量 \(V\) 所在层中,若块数多于 \(\log V\) 个,则所有点均不可能访问所有点一次,因为储水量减半,块数只会从原来的块分裂成更多块,从而导致选出 \(\log V\) 个块根本不可能覆盖全集,所以只需枚举初始时的每个块,判定每个块是否能到达所有点即可。

时空复杂度 \(O((n+V)\log V)\)

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int D=20;
constexpr int V=(1<<19)+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;int f=1;
	char ch=getchar();
	while (ch<48) {if(ch=='-')f=-1;ch=getchar();} 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();x*=f;
}

int n,m,d;
int a[N],len[D],num[V];
int L[D][N],R[D][N],id[D][N];
int dpL[V],dpR[V];

int main() {
	read(n);read(m);
	f(i,1,n) read(a[i]);
	while (1) {
		int cnt=0;
		f(i,1,n) {
			if (i==1||a[i]-a[i-1]>m) cnt++;
			id[d][i]=cnt;
			if (!L[d][cnt]) L[d][cnt]=i;
			R[d][cnt]=i;
		}
		len[d]=cnt;
		if (!m) break;
		m/=2;d++;
	}
	if (len[0]>d+1) {
		f(i,1,n) puts("Impossible");
		return 0;
	}
	if (len[0]==1) {
		f(i,1,n) puts("Possible");
		return 0;
	}
	f(i,0,d) num[1<<i]=i+1;
	dpL[0]=n+1;dpR[0]=0;
	const int S=(1<<d)-1;
	f(s,1,S) {
		dpL[s]=n+1;
		int val=s;
		while (val) {
			int lb=(val&-val),i=num[lb];val^=lb;
			dpR[s]=max(dpR[s],dpR[s^lb]);
			dpL[s]=min(dpL[s],dpL[s^lb]);
			dpR[s]=max(dpR[s],R[i][id[i][dpR[s^lb]+1]]);
			dpL[s]=min(dpL[s],L[i][id[i][dpL[s^lb]-1]]);
		}
	}
	f(i,1,len[0]) {
		int flag=0,l=L[0][i],r=R[0][i];
		f(s,0,S) if (dpR[s]+1>=l&&dpL[S^s]-1<=r) {flag=1;break;}
		if (flag) {f(i,l,r) puts("Possible");}
		else {f(i,l,r) puts("Impossible");}
	}
	return 0;
}

Alice and Recoloring 1 & Alice and Recoloring 2

题意

Easy Version:

给定一个 \(n\times m\)\(0,1\) 矩阵,你可以进行一下几种操作:

  • 花费 \(1\) 的代价,选择一个左上矩阵,将矩阵内所有元素反转;
  • 花费 \(2\) 的代价,选择一个左下矩阵,将矩阵内所有元素反转;
  • 花费 \(4\) 的代价,选择一个右上矩阵,将矩阵内所有元素反转;
  • 花费 \(3\) 的代价,选择一个右下矩阵,将矩阵内所有元素反转。

求将矩阵内所有元素置为 \(0\) 的最小代价。

Hard Version:

与简单版的区别仅在于,提供的操作方案变成:

  • 花费 \(1\) 的代价,选择一个左上矩阵,将矩阵内所有元素反转;
  • 花费 \(3\) 的代价,选择一个左下矩阵,将矩阵内所有元素反转;
  • 花费 \(4\) 的代价,选择一个右上矩阵,将矩阵内所有元素反转;
  • 花费 \(2\) 的代价,选择一个右下矩阵,将矩阵内所有元素反转。

\(1\le n,m\le 500\)

solution

先考虑简单版怎么做。

发现对左下矩阵与右上矩阵进行反转都可以看作对两个左上矩阵反转,而反转左下矩阵或右上矩阵的代价都不小于两次反转左上矩阵的代价,所以反转左下矩阵或右上矩阵没有意义,不用考虑。

考虑进行差分,令 \(a_{i,j}\) 为原矩阵的元素,则 \(b_{i,j}=a_{i,j}\oplus a_{i+1,j}\oplus a_{i,j+1}\oplus a_{i+1,j+1}\),其中 \(\oplus\) 表示异或操作。

我们需要将 \(b\) 这个矩阵内的元素全变成 \(0\)

那么在 \(a\) 上进行一个左上矩阵的反转相当于在 \(b\) 上对一个位置反转,在 \(a\) 上进行一个右下矩阵的反转相当于在 \(b\) 上对一个矩形的四个端点进行反转,其中矩形的右下端点为矩阵右下角元素,将左上矩阵的反转称为操作一,右下矩阵的反转称为操作二。

发现操作一代价为 \(1\),反转一个位置,操作二代价为 \(3\),反转四个位置,看似操作二更优,实际上操作二一定不会超过一次,因为若进行两次,则右下角元素进行了两次反转,不发生变化,那么可以反转的位置数量只有 \(6\) 个,代价也为 \(6\),不小于直接进行操作 \(1\) 的代价,所以只需要判断能否找到一个矩形,右下端点为右下角元素且四个端点均为 \(1\),若找到则进行一次操作二使其反转,否则不操作,其余的所有 \(1\) 使用操作 \(1\) 进行反转即可。

时间复杂度 \(O(nm)\)

Code
#include<cstdio>
#include<algorithm>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=500+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m,ans,flag=1;
bitset<N> a[N],b[N];

int main() {
	read(n);read(m);
	f(i,1,n) {
		static char in[N];
		scanf("%s",in+1);
		f(j,1,m) a[i].set(j,in[j]=='B');
	}
	f(i,1,n) f(j,1,m) ans+=(b[i][j]=(a[i][j]^a[i+1][j]^a[i][j+1]^a[i+1][j+1]));
	f(i,1,n-1) f(j,1,m-1) if (b[i][j]&&b[n][m]&&b[i][m]&&b[n][j]) {ans-=flag;flag=0;}
	printf("%d\n",ans);
	return 0;
}

困难版的区别在于,对操作二的代价变成了 \(2\)

先找找有什么性质,令操作二 \((i,j)\) 表示对 \((i,j),(i,m),(n,j),(n,m)\) 这个四个点同时反转:

  • 对于两个操作二 \((i_1,j_1),(i_2,j_2)\)\(i_1=i_2\) 一定不优,因为这样同时反转了 \((i_1,m),(n,m)\) 这两个点两次,这两个点状态不变,那么相当于用 \(4\) 的代价修改了 \(4\) 个位置,不优于直接使用四次操作一;
  • 对于一个操作二 \((i,j)\),若 \((i,j),(i,m),(n,j)\) 中有一个点不为 \(1\) 则一定不优,因为这样即使使其他三个点都从 \(1\) 变成 \(0\),那么相当于用了 \(2\) 的代价减少了两个 \(1\)(因为将一个 \(0\) 反转成了 \(1\)),由于第一条限制,不会再通过其他操作二将从 \(0\) 变成 \(1\) 这个位置重新变成 \(0\),说明反转出来这个 \(1\) 一定由操作一消除,那么总共使用了 \(3\) 的代价消去了 \(3\)\(1\),不优于直接使用三次操作一;
  • 对于一个操作二 \((i,j)\),若 \((i,j),(i,m),(n,j)\) 均为 \(1\),则使用一次操作二一定不劣,因为这样用 \(2\) 的代价至少消去了 \(2\)\(1\),不劣于使用操作一。

由以上三条性质,我们可以尽可能多的找到操作二 \((i,j)\),满足没有两个操作二 \(i\) 相等或 \(j\) 相等,同时满足 \((i,j),(i,m),(n,j)\) 均为 \(1\),经典地转化为行和列的二分图匹配问题,源点侧点表示行号,汇点侧点表示列号,一条边从 \(i\) 连向 \(j\) 的边表示一个操作二 \((i,j)\),得到的最大匹配即为操作二的方案,进行完操作二后再对剩余的 \(1\) 使用操作一即可。

使用 dinic 求二分图最大匹配,时间复杂度 \(O(n^2\sqrt n)\)\(n,m\) 同阶)。

Code
#include<cstdio>
#include<algorithm>
#include<queue>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=500+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int s,t,cnt;
namespace Flow {
	constexpr int N=3e5+10;
	constexpr int M=3e6+10;
	int head[N],tot=1;
	struct Edge{int next,to,w;}e[M];
	
	inline void add(int u,int v,int w) {
		e[++tot]={head[u],v,w};
		head[u]=tot;
		e[++tot]={head[v],u,0};
		head[v]=tot;
	}
	
	int dis[N],del[N];
	inline bool spfa() {
		f(i,1,cnt) dis[i]=inf;
		dis[s]=0;
		del[s]=head[s];
		queue<int> q;
		q.push(s);
		while (!q.empty()) {
			int now=q.front();
			q.pop();
			for (int i=head[now];i;i=e[i].next) {
				int v=e[i].to,w=e[i].w;
				if (dis[v]!=inf||!w) continue;
				dis[v]=dis[now]+1;
				del[v]=head[v];
				q.push(v);
				if (v==t) return 1;
			}
		}
		return 0;
	}
	
	inline int dfs(int now,int sum) {
		if (now==t) return sum;
		int res=0;
		for (int i=del[now];i&&sum;i=e[i].next) {
			del[now]=i;
			int v=e[i].to,w=e[i].w;
			if (dis[v]!=dis[now]+1||!w) continue;
			int k=dfs(v,min(sum,w));
			if (!k) dis[v]=inf;
			e[i].w-=k;e[i^1].w+=k;
			res+=k;sum-=k;
		}
		return res;
	}
	
	inline int dinic() {
		int res=0;
		while (spfa()) res+=dfs(s,1e9);
		return res;
	}
}

int n,m,ans;
int idx[N],idy[N];
bitset<N> a[N],b[N];
using Flow::add;

int main() {
	read(n);read(m);
	f(i,1,n) {
		static char in[N];
		scanf("%s",in+1);
		f(j,1,m) a[i].set(j,in[j]=='B');
	}
	f(i,1,n) f(j,1,m) b[i][j]=(a[i][j]^a[i+1][j]^a[i][j+1]^a[i+1][j+1]);
	s=++cnt;t=++cnt;
	f(i,1,n-1) {
		idx[i]=++cnt;
		add(s,cnt,1);
	}
	f(i,1,m-1) {
		idy[i]=++cnt;
		add(cnt,t,1);
	}
	f(i,1,n-1) f(j,1,m-1) if (b[i][j]&&b[i][m]&&b[n][j]) add(idx[i],idy[j],1);
	int k=Flow::dinic();
	b[n][m]=b[n][m]^(k&1);
	f(i,1,n) f(j,1,m) ans+=b[i][j];
	printf("%d\n",ans-k);
	return 0;
}

Frequency Problem (Easy Version) & Frequency Problem (Hard Version)

题意

给定一个长度为 \(n\) 的数组 \(a\),求出最长的子数组满足众数不唯一。

\(1\le n\le 2\times 10^5\)

Easy Version:\(1\le a_i\le \min(100,n)\)

Hard Version:\(1\le a_i\le n\)

solution

考虑简单版怎么做。

发现答案区间的众数包含整个数组的众数一定不劣(若整个数组众数不唯一则答案为 \(n\),所以只考虑整个数组众数唯一的情况),考虑反证,若一个区间众数不为整个数组的众数时最优,令区间众数出现次数为 \(x\),区间中出现整个数组的众数次数为 \(y\),则初始时有 \(x-y>0\),那么不断扩展这个区间,当扩展到整个数组时有 \(x-y<0\),则扩展过程中一定能够找到一个时刻满足 \(x-y=0\),由于扩展过程中区间长度不降,所以 \(x-y=0\) 时区间长度大于初始区间长度,与初始区间最优矛盾。

由于最多只有 \(100\) 种不同的数,则枚举每种数,钦定它和整个数组众数同时成为答案区间的众数,令当前枚举的数为 \(x\),整个数组众数为 \(y\),那么需要找到一个数组满足 \(x\) 出现次数与 \(y\) 相同,那么从左到右扫描,维护当前 \(x\) 出现次数与 \(y\) 出现次数之差,令这个差为 \(k\),则如果前面有一个位置差为 \(-k\),则前面对应位置与当前位置之间的子数组满足条件,只需维护每个 \(k\) 第一次出现位置即可,若这个子数组内出现最多的数不是 \(x\),那么通过上面提到的扩展,可以证明答案一定不小于这个子数组的长度,所以不会影响答案。

时间复杂度 \(O(nV)\)

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
int a[N],t[N];

inline int calc(int x,int y) {
	static int s[N<<1];s[n]=1;
	int now=n,val=n*2,ret=0;
	f(i,1,n) {
		if (a[i]==x) now++;
		if (a[i]==y) now--;
		if (s[val-now]) ret=max(ret,i-s[val-now]+1);
		else s[val-now]=i+1;
	}
	now=n;
	f(i,1,n) {
		if (a[i]==x) now++;
		if (a[i]==y) now--;
		s[val-now]=0;
	}
	return ret;
}

int main() {
	read(n);
	if (n==1) return puts("0"),0;
	f(i,1,n) read(a[i]),t[a[i]]++;
	int mx=0,id=0,ans=0;
	f(i,1,n) if (t[i]>mx) mx=t[i],id=i;
	f(i,1,n) if (i!=id&&t[i]) ans=max(ans,calc(i,id));
	printf("%d\n",ans);
	return 0;
}

困难版不能直接枚举每个数了,考虑根号分治。

对于答案区间中出现次数最多的数出现次数大于设定阈值 \(B\),这样的数最多有 \(\frac{n}{B}\) 个,使用简单版的做法计算贡献即可,这部分时间复杂度 \(O(\frac{n^2}{B})\)

那么我们还需要计算答案区间中出现次数最多的数出现次数不超过 \(B\) 的最长子数组长度。

考虑直接枚举答案区间中出现次数最多的数出现次数,双指针维护,令当前扫到了 \(r\),维护最小的 \(l\),满足 \([l,r]\) 所有数出现次数均不超过枚举的出现次数,若有不少于两个数出现次数等于枚举的次数,则更新答案,\(r\) 递增时 \(l\) 显然单调递增,且这样一定不会漏掉最优解,这部分时间复杂度 \(O(nB)\)

\(B=\sqrt n\) 得总时间复杂度 \(O(n\sqrt n)\)

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int B=400;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
int a[N],t[N];

inline int calc1(int x,int y) {
	static int s[N<<1];s[n]=1;
	int now=n,val=n*2,ret=0;
	f(i,1,n) {
		if (a[i]==x) now++;
		if (a[i]==y) now--;
		if (s[val-now]) ret=max(ret,i-s[val-now]+1);
		else s[val-now]=i+1;
	}
	now=n;
	f(i,1,n) {
		if (a[i]==x) now++;
		if (a[i]==y) now--;
		s[val-now]=0;
	}
	return ret;
}

inline int solve1(int id) {
	int ans=0;
	f(i,1,n) if (i!=id&&t[i]>=B) ans=max(ans,calc1(i,id));
	return ans;
}

inline int calc2(int tim) {
	static int t[N],v[N];
	int l=1,mx=1,ret=0;
	f(i,1,n) {
		v[t[a[i]]]--;
		v[++t[a[i]]]++;
		mx=max(mx,t[a[i]]);
		while (mx>tim) {
			v[t[a[l]]]--;
			v[--t[a[l]]]++;
			if (!v[mx]) mx--;
			l++;
		}
		if (v[tim]>=2) ret=max(ret,i-l+1);
	}
	f(i,1,n) v[i]=t[i]=0;
	return ret;
}

inline int solve2() {
	int ans=0;
	f(i,1,B) ans=max(ans,calc2(i));
	return ans;
}

int main() {
	read(n);
	f(i,1,n) read(a[i]),t[a[i]]++;
	if (n==1) return puts("0"),0;
	int mx=0,id=0;
	f(i,1,n) if (t[i]>mx) mx=t[i],id=i;
	printf("%d\n",max(solve1(id),solve2()));
	return 0;
}
posted @ 2026-07-30 14:24  Ff472130  阅读(14)  评论(0)    收藏  举报