The 2021 CCPC Weihai Onsite
B
发现要从集合中取子集,即取的数不能重复这个限制比较抽象,于是考虑容斥,先计算可重复的情况:(根据计算第二类斯特林数的启示,在可重复时,可直接按照k个数有标号考虑,去重之后直接除掉阶乘)
如果\(n=2^m-1\),那么前面\(k-1\)个数任取,最后一个数可以唯一确定最终的异或和。
从高位往低位,确定这k个数,发现只要在某位有任何一个数已经小于n,那么这个数之后的位可以任取,那只要保证其它的数后面的位合法,这个数就可以唯一确定最后的异或和的后面的位(该位由小于n的数的个数确定,前面的位由n和k的奇偶性决定)。
于是枚举第一个这样的位(和数位DP类似,这里变成枚举k个数和n的lcp),然后用一个\(O(k)\)的DP可以算出\(1,2,...,k\)个有标号可重复的数,足题目条件的方案数(只算单个k的直接枚举该位小于n的数的个数,但由于容斥需要算所有\(1,2,...,k\)的,故用DP)
然后就是容斥了!对于相等关系,从无向图的角度看,考虑对\(\frac{k(k-1)}{2}\)条边容斥,暴力的做法是枚举每条边是否存在,然后计算对应图的点数为奇数的连通块个数(偶数的连通块所有数取一个值,对异或和没有影响,直接乘上n),对应上面的可重复情况的方案数。(虽然由于相等的传递性,实际的图只会是一个个团,但容斥时仍要考虑所有的图)
然后优化这个容斥,因为只关心点数为奇数的连通块个数,考虑\(f[i][j]\)表示j个点组成i个奇数的连通块的容斥系数之和,发现关键在于求出一个某点数(记为\(s\))的连通块的系数和。
这个就是所有大小为\(s\)的连通图,每个的贡献为\((-1)^{边数}\)的和。用经典的考虑一号点所在连通块的大小的做法,结合所有图的和是\((-1+1)^{\frac{s(s-1)}{2}}\)的观察,就可以推出容斥系数总和为\((-1)^{s-1}(s-1)!\)
C
挺有意思的题,但要在场上切难度还是挺大的(和前十题不在一个档次)
转成原根后变成模意义下的01背包(原本考虑了一下转生成函数,但是得保留\(np\)位,显然不靠谱;而bitset优化暴力又过不去)。
发现做这个背包的过程是或运算,也就是说有用的操作只会把某些0变成1,那么自然地会想到:每次用\(Poly(log)\)的时间找到要改变的位置,这样总的复杂度就是\(O(pPoly(log))\)的。
但这个要改变的位置的条件比较抽象,是\(f[i]=0,f[i-x]=1\)(另一种情况同理),考虑一遍跳过去的话,很难直接找到下一个满足这个条件的位置。
但通过联想一些做题经验(比如CF-R857-E),发现要找到下一个两段不同的位置是容易的(二分+哈希BIT即可),即\(f[i]=0,f[i-x]=1 或 f[i]=1,f[i-x]=0\)
那如果直接这么找,再判断一下,复杂度如何呢?感性认知上挺对的;实际上:转移形成的有向图形成若干置换环,那么这两种边的个数是一样的。所以总复杂度就是\(O(plog^2p+n)\)了!
实现有些小细节,比如转成原根之后是在\(p-1\)的环上走;不同起点的哈希判断要平移(这个判错会导致复杂度不对而TLE,然后以为其它错导致的);没有赋值操作的特判等。(其实想清楚了也都还好)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
inline int fpw(int a,int x,int P){
int s=1;
for(;x;x>>=1,a=1ll*a*a%P) if(x&1) s=1ll*s*a%P;
return s;
}
inline int prd(int a,int b,int P){return 1ll*a*b%P;}
#define repeat(i,a,b) for(int i=a;i<b;i++)
typedef long long ll;
ll getG(ll n){ // 求 n 最小的原根
static vector<ll> a; a.clear();
ll k=n-1;
repeat(i,2,sqrt(k+1)+1)
if(k%i==0){
a.push_back(i); // a 存放 (n-1) 的质因数
while(k%i==0)k/=i;
}
if(k!=1)a.push_back(k);
repeat(i,2,n){ // 枚举答案
bool f=1;
for(auto j:a)
if(fpw(i,(n-1)/j,n)==1){
f=0;
break;
}
if(f)return i;
}
return -1;
}// BSGS,a 和 mod 互质
typedef unsigned long long ul;
const int N=2e6+5;
const ul B=793999;
struct BIT{
int n;
ul a[N];
void init(int m){
n=m;
for(int i=0;i<=n;i++) a[i]=0;
}
void upd(int x,ul y){
//cout<<"upd:"<<x<<" "<<y<<endl;
x++;
while(x<=n) a[x]+=y,x+=x&-x;
}
ul cnt(int x){
x++;
ul s=0;
while(x) s+=a[x],x-=x&-x;
return s;
}
}T;
ul pw[N];
int P,G,n,a[N],id[N];
int f[N];
ul cnt(int l,int r){
//cout<<"cnt:"<<l<<" "<<r<<" "<<T.cnt(r)-T.cnt(l-1)<<endl;
return (T.cnt(r)-T.cnt(l-1))*pw[P-1-r];
}
stack<int>q;
int sum;
void work(int x,int last,int end){
// cout<<x<<" "<<last<<" "<<end<<endl;
int cal=0;
while(last<end){
// cout<<"last="<<last<<endl;
cal++;
if(cal>P) return;
int l=1,r=end-last;
while(l<r){
int mid=(l+r)>>1;
if(cnt(last+1,last+mid)==cnt(last-x+1,last-x+mid)) l=mid+1;
else r=mid;
}
// cout<<"l="<<l<<endl;
if(cnt(last+1,last+l)==cnt(last-x+1,last-x+l)) last=end;
else{
//puts("!");
last=last+l;
//cout<<last<<endl;
if(!f[last]) q.push(last);
}
}
sum+=cal;
}
int main() {
cin>>P>>n;
pw[0]=1; for(int i=1;i<P;i++) pw[i]=pw[i-1]*B;
G=getG(P);
//if(n==100000) cout<<"G="<<G<<endl;
int s=1;
for(int i=0;i<P-1;i++,s=prd(s,G,P)) id[s]=i;
T.init(P-1);
int tot=0,ans=1,vs=0;
for(int i=1;i<=n;i++){
int op,x;
scanf("%d%d",&op,&x);
if(!x){
ans=0;
continue;
}
x=id[x];
//cout<<"x="<<x<<endl;
if(op==0){
vs=1;
if(f[x]) continue;
f[x]=1;
T.upd(x,pw[x]);
}
else a[++tot]=x;
}
// cout<<ans<<endl;
if(!vs){
cout<<P-1<<endl;
return 0;
}
for(int i=1;i<=tot;i++){
int x=a[i];
work(x,x-1,P-2);
work(x-(P-1),-1,x-1);
while(!q.empty()){
int u=q.top();
q.pop();
if(f[u]) continue;
f[u]=1;
T.upd(u,pw[u]);
}
//if(n==100000 && i%1000==0) cout<<"i="<<i<<" sum="<<sum<<endl;
}
for(int i=0;i<P-1;i++) if(!f[i]) ans++;
cout<<ans<<endl;
return 0;
}
L
一般的最大团问题是NPC的,考虑题目描述了什么性质:每次只连相邻两个点后交换,连边和点的编号顺序有关;先把无向边按小->大定向,考虑若有a->c(a<c),对于中间的b,必须有a->b或b->c
对这样的性质,考虑补图,即补图是有传递闭包的性质的DAG!
那么原图最大团=补图最大独立集=(定向后)DAG最长反链=DAG最小连覆盖=V-最大匹配
然后就是去掉m条边的竞赛图求最大匹配:首先对左边的每个点先贪心匹配,配完只会剩至多\(2\sqrt{m}\)个点没被配到(考虑度数小于\(\sqrt{m}\)的点,最多有\(\sqrt{m}\)个没被匹配,然后度数大于\(\sqrt{m}\)的点又至多\(\sqrt{m}\)个),对没配到的点暴力增广即可(单次增广的复杂度是\(O(边数)\),在这里用并查集维护链表,使得每次通过没删的边,都指向后面第一个没被访问的点,即可保证复杂度)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,a[N];
unordered_map<int,bool>mp[N];
int s[N],t[N],nx[N];
int find(int u){
if(u==nx[u]) return u;
return nx[u]=find(nx[u]);
}
int dcnt,dfn[N];
bool dfs(int x){
for(int p=find(x+1);p<=n;p=find(p)) {
if(mp[x].count(p)){
p++; continue;
}
if(dfn[p]!=dcnt){
dfn[p]=dcnt;
nx[p]=p+1;
if(!t[p] || dfs(t[p])){
t[p]=x;
return 1;
}
}
}
return 0;
}
void work(){
cin>>n>>m;
for(int i=1;i<=n;i++) a[i]=i;
for(int i=1;i<=m;i++){
int x;
scanf("%d",&x);
int u=a[x],v=a[x+1];
if(u>v) swap(u,v);
mp[u][v]=1;
swap(a[x],a[x+1]);
}
int ans=0;
for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++) if(!mp[i].count(j) && !t[j]){
s[i]=j; t[j]=i;
ans++;
break;
}
for(int i=1;i<=n;i++) if(!s[i]){
dcnt++;
for(int j=1;j<=n+1;j++) nx[j]=j;
if(dfs(i)) ans++;
}
cout<<n-ans<<endl;
}
int main()
{
work();
return 0;
}
浙公网安备 33010602011771号