一些题目
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}\) 要求 \([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;
}