一些题目

Function Query

定义 \(f(x)=(x\oplus a)-b\),其中 \(a,b\) 是给定的参数。

给定 \(n\) 个变量,\(x_1,x_2,x_3,\dots,x_n\),给出 \(q\) 组询问,对于每组询问,给定 \(a,b\),请你输出一个 \(i\),满足 \(i\in [1,n)\),且有 \(f(x_i)\times f(x_{i+1})\le 0\),如果无解则输出 \(-1\),如果有多组解输出任意一个即可。


很好的题目,根本不知道要用什么算法。思考过 Trie 树,但是感觉没有什么用。

看了题解,这东西竟然能二分????

有个很重要的结论:

如果 \(\max f(x)\times \min f(x)\le0\),那么肯定有解。

看上去非常不对,不是要相邻才行吗?

实际上无解的情况当且仅当所有 \(f(x)\) 同号,上面那个式子成立了就说明有正有负或者有零。

然后就可以对这个东西二分了。

找到 \(\max f(x)\)\(\min f(x)\) 的位置,这个东西可以用 Trie 树搞。

不妨把它们的位置设为 \([l,r]\),那么 \([l,r]\) 长度为 \(2\) 的时候 \(l\) 就是答案了。

考虑不断去缩小这个区间。取 \(mid=\lfloor\frac{l+r}{2}\rfloor\)\(f(x_l)\times f(x_{mid})\le 0\)\(f(x_{mid})\times f(x_r)\le 0\) 一定至少有一个成立,哪个成立往哪边缩小就好了。

代码的实现并不困难,时间复杂度 \(\mathcal O(q(\log V+\log n))\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e6+10;
int n,q,tot;
int trie[N][2],id[N],a[N];
void add(int x,int y)
{
	int now=0;
	for(int i=30;i>=0;i--)
	{
		int c=(x>>i)&1;
		if(!trie[now][c])trie[now][c]=++tot;
		now=trie[now][c];
	}
	id[now]=y;
}
int find(int x,int op)
{
	int now=0;
	for(int i=30;i>=0;i--)
	{
		int c=(x>>i)&1^op;
		if(trie[now][c])now=trie[now][c];
		else now=trie[now][!c];
	}
	return id[now];
}
bool check(int x,int y,int l,int r)
{
	return ((a[l]^x)-y)*((a[r]^x)-y)<=0;
}
signed main()
{
	cin>>n>>q;
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		add(a[i],i);
	}
	while(q--)
	{
		int x,y;
		scanf("%lld%lld",&x,&y);
		int l=find(x,0),r=find(x,1);
		if(l>r)swap(l,r);
		if(!check(x,y,l,r))
		{
			puts("-1");
			continue;
		}
		while(l+1<r)
		{
			int mid=(l+r)/2;
			if(!check(x,y,l,mid))l=mid;
			else r=mid;
		}
		cout<<r-1<<endl;
	}
	return 0;
}

P6008 [USACO20JAN] Cave Paintings P

一种没有见过的 dp,记录一下。神 @aioilit 教会了我。


自底向上进行 dp 是显然的,考虑对每个连通块计算贡献。

以样例为例,对于 #.#...#.#\(3\) 个连通块,每个联通块都有画和不画两种选择。

任意两个连通块不干扰,所以总方案是 \(2^3=8\)

思考如何转移。现在多了一行 #...#...#,和刚刚那行连起来就只剩一个连通块了。所以可以看做这一层只有一个节点,有 \(3\) 个儿子。

所以转移方程是很朴素的 \(dp_x=\prod dp_i+1\)\(i\) 是能与当前连通块 \(x\) 相连的另一个连通块。\(+1\) 是因为可以把整个连通块都画上。

用并查集维护即可,讲的可能不是很好,细节见代码吧。

#include<bits/stdc++.h>
#define int long long
#define id(x,y) ((x-1)*m+y)
using namespace std;
const int N=1010,mod=1e9+7;
int n,m;
int a[N][N],f[N*N],dp[N*N];
int find(int k)
{
	return f[k]==k?k:f[k]=find(f[k]);
}
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
			char c;
			cin>>c;
			a[i][j]=c=='.';
			f[id(i,j)]=id(i,j);
			dp[id(i,j)]=1;
		}
	for(int i=n;i>=1;i--)
	{
		for(int j=3;j<m;j++)
			if(a[i][j]&&a[i][j-1])
				f[find(id(i,j))]=find(id(i,j-1));//同一个连通块
		for(int j=2;j<m;j++)
			if(a[i][j]&&a[i+1][j])
			{
				int x=find(id(i,j)),y=find(id(i+1,j));
				if(x==y)continue;//同一个连通块无需转移
				f[y]=x;
				dp[x]*=dp[y],dp[x]%=mod;//不同的连通块不干涉,相乘即可
			}
		for(int j=2;j<m;j++)
			if(a[i][j]&&find(id(i,j))==id(i,j))
				dp[id(i,j)]++;//在根节点计算都画上的贡献
	}
	int ans=1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			if(a[i][j]&&find(id(i,j))==id(i,j))
				ans*=dp[id(i,j)],ans%=mod;
	cout<<ans;
	return 0;
}

P8945 Inferno

很好的一道题,很符合我对 CCF 比赛的想象,虽然和 CCF 没有什么关系。

\(\mathcal O(n^2)\) 是好想的,直接暴力枚举两个端点就行。关键代码:

int ask(int l,int r)
{
	int sum=f[r]-f[l-1],s=g[r]-g[l-1];
	if(s<=m)return sum+s;
	return sum+m-(s-m);
}

然后发现这个东西要分两类,不太能优化,然后就不会了,遂打开题解。

看来我是真不会做这种题了。我他妈直接对两种情况分开做分别计算贡献不就好了????

\(f_i\) 为把所有 \(0\) 都变成 \(1\) 的前缀和,\(g_i\) 为把所有 \(0\) 都变成 \(-1\) 的前缀和。

然后两类就变成了 \(f_r-f_{l-1}\)\(g_r-g_{l-1}+2\times k\)

前者取到要求 \(0\) 的个数小于等于 \(k\),然后发现对于一个 \(r\) 能取的 \(l\) 是一段区间,单调队列进行优化即可。

后者取到要求 \(0\) 的个数大于 \(k\),然后发现对于一个 \(r\) 能取的 \(l\) 是一段前缀,求前缀最小值即可。

最后就做完了,为什么我想不到????

#include<bits/stdc++.h>
using namespace std;
const int N=1e7+10;
int n,k,ans=1;
int a[N],f[N],g[N],s[N];
void solve()
{
	int now=0,mi=1e9;
	for(int i=1;i<=n;i++)
	{
		for(;now<=i&&s[i]-s[now]>k;now++)
			mi=min(mi,g[now]);
		ans=max(ans,g[i]-mi+2*k);
	}
}
void Solve()
{
	deque<int>q;
	for(int i=1;i<=n;i++)
	{
		while(!q.empty()&&s[i]-s[q.front()]>k)
			q.pop_front();
		while(!q.empty()&&f[i]<f[q.back()])
			q.pop_back();
		q.push_back(i);
		if(!q.empty())ans=max(ans,f[i]-f[q.front()]);
	}
}
int main()
{
	cin>>n>>k;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		f[i]=f[i-1]+a[i];
		g[i]=g[i-1]+a[i];
		s[i]=s[i-1]+!a[i];
		if(!a[i])f[i]++,g[i]--;
	}
	solve();
	Solve();
	cout<<ans;
	return 0;
}

P8162 [JOI 2022 Final] 让我们赢得选举 (Let's Win the Election)

其实这题还是不错的,但是当时写了个贪心的错解不知道错哪了,遂摆烂,启动题解。

先说说贪心的错解罢。

首先多个人一起演讲一定是最优的,这个没有问题。

然后先选 \(b\),再选 \(a\),这个结论也是正确的。

\(b\) 排序,暴力枚举有几个协作者。贪心选取前 \(i\) 个,贪心选取剩下的 \(k-i\) 个州。

但是这样为什么是错的呢?

观察下面这组样例:

3 3
5 5
1 1001
1002 1002

这个时候应当选第三个为协作者才优,因为第二个的 \(a\) 很小。

那就不能贪心了,考虑 dp。

排序方式不变,同样枚举选几个 \(b\)

然后有几个很显然的结论。

  • 如果确定了选哪些 \(b\),那么从小到大做一定更优。

  • 枚举了选几个 \(b\) 之后,就知道了其它每个州演讲的时间。即 \(\frac{a_i}{s}\),其中 \(s\) 是枚举的数量。

设计 \(dp_{i,j,k}\) 为做到第 \(i\) 个州,选了 \(j\)\(b\)\(k\)\(a\) 的最小演讲时间。

转移没什么好说的,但是时间复杂度是 \(\mathcal O(n^4)\) 的,无法通过。

状态已经是 \(\mathcal O(n^3)\) 的了,所以肯定要减少状态。

接着继续发现结论:

  • 假如最后一个选 \(b\) 的是第 \(i\) 位,那么 \([1,i-1]\) 不可能啥都不选。

不想证明,哈哈。但是应该可以感性理解:因为如果有,交换这两个州一定更优。

那么就变成了:设计 \(dp_{i,j}\) 为做到第 \(i\) 个州,选了 \(j\)\(b\) 的最小演讲时间。

有了上面那个结论的话转移是简单的:

\[dp_{i,j}=\min \{dp_{i-1,j}+\frac{a_i}{s},dp_{i-1,j-1}+\frac{b_i}{j}\} \]

但是还没有完全做完。

因为上面的 \(dp_{i,j}\) 要求 \([1,i]\) 全选,但是我们的结论只是知道 \(1\) 到最后一个选 \(b\) 的位置全选。

那就直接枚举最后一个选 \(b\) 的位置不就好了,然后在剩下的的州中选更小的 \(a\) 即可。

用优先队列直接搞的话是 \(\mathcal O(n^3\log n)\) 的,在 某个 OJ 无法通过。那就只好预处理了,时间复杂度 \(\mathcal O(n^3)\)

#include<bits/stdc++.h>
using namespace std;
const int N=510;
int n,m;
double ans=1e18;
double dp[N][N],f[N][N];
priority_queue<int,vector<int>,greater<int>>q;
struct ccf
{
	int x,y;
}a[N];
bool cmp(ccf a,ccf b)
{
	return a.y!=b.y?a.y<b.y:a.x<b.x;
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i].x>>a[i].y;
		if(a[i].y==-1)a[i].y=1e9;
	}
	sort(a+1,a+1+n,cmp);
	for(int i=0;i<=n;i++)
	{
		for(int j=i+1;j<=n;j++)
			q.push(a[j].x);
		int sum=0;
		for(int j=1;!q.empty();j++)
		{
			sum+=q.top();
			f[i][j]=sum;
			q.pop();
		}
	}
	for(int s=0;s<m;s++)
	{
		if(a[s].y==1e9)continue;
		for(int i=1;i<=n;i++)
			for(int j=0;j<=min(i,s);j++)
			{
				dp[i][j]=1e18;
				if(i!=j)dp[i][j]=dp[i-1][j]+1.0*a[i].x/(s+1);
				if(j)dp[i][j]=min(dp[i][j],dp[i-1][j-1]+1.0*a[i].y/j);
			}
		for(int i=s;i<=n;i++)
		{
			double sum=dp[i][s];
			ans=min(ans,sum+f[i][max(m-i,0)]*1.0/(s+1));
		}
	}
	cout<<ans;
	return 0;
}

强联通分量

MX 的题目,无原题。我怎么这么菜,如此状态,如何 NOIP???


小 O 有一张 \(n\) 个点的有向图,每个点 \(i\) 只有一条到 \(a_i\) 的出边。

但是由于一些原因,小 O 会从原图中删除若干个点及其相邻的边,导致这张图变得不完整。

现在,小 O 想要知道,对于所有 \(2^n\) 种删除点的方案,这张有向图的强连通分量的个数的和对 \(10^9+7\) 取模后的值。


非常困难,以为是容斥。题解短短几行略过,被羞辱了。

首先这是个基环树森林,然后有个重要的结论:

强连通分量的个数=点数-在环上的点数+环的个数

题解表示分开计算即可,但是我不会啊???

原来这种题目可以分开计算,长见识了。

点数是 \(\sum\limits_{i=1}^n i\binom{n}{i}\),在环上的点数是 \(\sum\limits_{i=1}^{tot}f_i\times2^{n-f_i}\),环的个数是 \(\sum\limits_{i=1}^{tot}2^{n-f_i}\)\(tot\) 是环的个数,\(f_i\) 是环的大小。枚举的 \(i\) 就是钦定了选第 \(i\) 个环并考虑这个环对答案的贡献,其它不在环上的可以随便选,就是 \(2^{n-f_i}\)

我只会 topo 找环怎么办,代码过于冗长。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10,mod=1e9+7;
int n,cnt,siz,tot,ans;
int to[N],in[N],vis[N],f[N],fac[N],inv[N],p[N];
void init()
{
	fac[0]=fac[1]=inv[0]=inv[1]=p[0]=1;
	p[1]=2;
	for(int i=2;i<=n;i++)
	{
		fac[i]=fac[i-1]*i%mod;
		inv[i]=(mod-mod/i)*inv[mod%i]%mod;
		p[i]=p[i-1]*2%mod;
	}
	for(int i=2;i<=n;i++)
		inv[i]*=inv[i-1],inv[i]%=mod;
}
void topo()
{
	queue<int>q;
	for(int i=1;i<=n;i++)
		if(!in[i])q.push(i);
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		vis[x]=1,cnt++;
		in[to[x]]--;
		if(!in[to[x]])
			q.push(to[x]);
	}
}
void dfs(int x)
{
	siz++,vis[x]=1;
	if(!vis[to[x]])
		dfs(to[x]);
}
int C(int x,int y)
{
	return fac[x]*inv[y]%mod*inv[x-y]%mod;
}
signed main()
{
	freopen("scc.in","r",stdin);
	freopen("scc.out","w",stdout);
	cin>>n;
	init();
	for(int i=1;i<=n;i++)
		scanf("%lld",&to[i]),in[to[i]]++;
	topo();
	for(int i=1;i<=n;i++)
	{
		if(vis[i])continue;
		siz=0,dfs(i),cnt++;
		f[++tot]=siz;
	}
	for(int i=1;i<=n;i++)
		ans+=i*C(n,i)%mod,ans%=mod;
	for(int i=1;i<=tot;i++)
	{
		ans-=f[i]*p[n-f[i]]%mod-mod;
		ans+=p[n-f[i]];
		ans%=mod;
	}
	cout<<ans;
	return 0;
}
posted @ 2024-10-29 09:35  菲斯斯夫斯基  阅读(21)  评论(0编辑  收藏  举报