【考試總結】2022-05-15
出題人
如果 \(\exists a_i\equiv0\bmod 2\) 那麼可以讓 \(b_i=\frac{a_i}2\),剩下的 \(b_j=a_j-b_i\)
其實問題就是找到一個 \(\{a\}\) 的子集 \(S\) 使得存在 \(b\) 滿足 \(b_1+b_2=S_1,b_2+b_3=S_2,\dots b_{|S|}+b_1=S_{|S|}\)
這時候 \(S\) 注意這裡的等式一定是全集構成一個整環而不存在枝杈,否則讓基環森林裡面的任意一個環滿足即可
繼續觀察上面的等式發現:由於 \(S_i\) 全是奇數所以 \(|S|\) 是偶數,將 \(S_i\) 按照下標奇偶性分成兩組,那麼兩組的和均為 \(\sum b\)
那麼現在問題就是在 \(\{a\}\) 中找到兩個權值和大小都相同的子集,通過列舉子集的方式可以規避三進位制,使用雜湊表儲存資訊即可
卡常技巧包括但不限於將使用雜湊表的一側大小設為 \(\frac{n}2-2\) 或者當程式執行時間超過閥值輸出 NO
Code Display
const int N=400; int n,a[N],b[N],v[N]; pair<int,int> ans[N]; unordered_map<int,pair<int,int> > mp[N]; int sum1[1<<15],sum2[1<<17]; signed main(){ freopen("problemsetter.in","r",stdin); freopen("problemsetter.out","w",stdout); n=read(); rep(i,1,n) a[i]=read(); rep(i,1,n) if(a[i]%2==0){ puts("Yes"); vector<int> b={a[i]/2}; vector<pair<int,int> > ans; rep(j,1,n){ if(i==j) ans.emplace_back(1,1); else{ b.emplace_back(a[j]-b[0]); ans.emplace_back(1,b.size()); } } rep(e,0,n-1) print(b[e]); putchar('\n'); rep(e,0,n-1) print(ans[e].fir),print(ans[e].sec),putchar('\n'); exit(0); } if(n<=2) puts("No"),exit(0); int n1=max(1ll,n/2-2),n2=n-n1; int U1=1<<n1,U2=1<<n2; rep(s,1,U1-1){ int lb=s&(-s),id=__builtin_ctzll(lb)+1; sum1[s]=sum1[s^lb]+a[id]; } rep(i,n1+1,n) b[i-n1]=a[i]; rep(s,1,U2-1){ int lb=s&(-s),id=__builtin_ctzll(lb)+1; sum2[s]=sum2[s^lb]+b[id]; } auto output=[&](const pair<int,int> S,const pair<int,int> T){ if(S.fir+S.sec+T.sec+T.fir==0) return ; vector<pair<int,int> >Plu,Min; for(int i=1;i<=n1;++i){ if(S.fir>>(i-1)&1) Plu.emplace_back(a[i],i); if(S.sec>>(i-1)&1) Min.emplace_back(a[i],i); } for(int i=1;i<=n2;++i){ if(T.fir>>(i-1)&1) Plu.emplace_back(b[i],i+n1); if(T.sec>>(i-1)&1) Min.emplace_back(b[i],i+n1); } sort(Plu.begin(),Plu.end()); sort(Min.begin(),Min.end()); int num=0,cur=0; b[++num]=cur; int siz=Plu.size(); while(Min.size()||Plu.size()){ if(Plu.size()==Min.size()){ cur=Plu.back().fir-cur; ans[Plu.back().sec]={num,num+1}; Plu.pop_back(); }else{ cur=Min.back().fir-cur; ans[Min.back().sec]={num,num==siz*2?1:num+1}; Min.pop_back(); } if(num<siz*2) b[++num]=cur; } for(int i=1;i<=n;++i) if(!ans[i].fir){ ans[i]={1,++num}; b[num]=a[i]-b[1]; } puts("Yes"); for(int i=1;i<=n;++i) print(b[i]); putchar('\n'); for(int i=1;i<=n;++i) print(ans[i].fir),print(ans[i].sec),putchar('\n'); exit(0); }; rep(S,0,U1-1){ for(int T=S;;T=(T-1)&S){ int num=__builtin_popcount(T)-__builtin_popcount(S^T); mp[n+num][sum1[T]-sum1[S^T]]=make_pair(T,S^T); if(!T) break; } } rep(S,0,U2-1){ for(int T=S;;T=(T-1)&S){ int num=__builtin_popcount(T)-__builtin_popcount(S^T); if(mp[n-num].count(-sum2[T]+sum2[S^T])){ output(mp[n-num][-sum2[T]+sum2[S^T]],make_pair(T,S^T)); } if(!T) break; } if(1.0*clock()/CLOCKS_PER_SEC>1.6) break; } puts("No"); return 0; }
彩色掛飾
建立圓方樹,設 \(f_{x,c}\) 表示 \(x\) 或 \(x\) 表示點雙的根的顏色為 \(c\) 時最少的聯通塊數
對於實點 \(x\) 可以直接合並下轄虛點的資訊即 \(\sum_{v} f_{v,c}\)
對於虛點 \(x\) 需要對聯通分量裡面的異色聯通塊數進行計算:
預處理出來點雙裡面每個子集的匯出子圖形成的聯通塊數量,以及每個子集填入哪種合法的顏色形成的聯通塊最少,再進行一個列舉子集再列舉子集填哪種顏色的 \(\rm DP\) 來計算貢獻
常見的建立圓方樹的過程就能讓父親作為方點出邊 vector
中的第一個點,要注意新增子集的過程中強制讓包含 \(1\) 元素的子集填成所需顏色
注意這樣的統計方式每個方點的父親會被算兩次,但是根這個實點只會被算一次,需要根據實際含義再加加減減
Code Display
const int N=2e5+10,inf=2e5+10;
int n,m,k,s;
vector<int> G[N],g[N];
int dp[N][25];
int col[N][64],con[N][64];
int dfn[N],low[N],stk[N],top,nds,tim;
int fa[N],ini[N];
unordered_set<int> edge[N];
bool exi[10][10];
inline int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
inline void tarjan(int x){
dfn[x]=low[x]=++tim; stk[++top]=x;
for(auto t:g[x]){
if(!dfn[t]){
tarjan(t); ckmin(low[x],low[t]);
if(low[t]>=dfn[x]){
++nds;
G[nds].emplace_back(x);
G[x].emplace_back(nds);
do{
G[nds].emplace_back(stk[top]);
G[stk[top]].emplace_back(nds);
--top;
}while(stk[top+1]!=t);
int siz=G[nds].size(),U=1<<siz;
for(int i=0;i<siz;++i){
for(int j=i+1;j<siz;++j){
exi[i][j]=exi[j][i]=edge[G[nds][i]].find(G[nds][j])!=edge[G[nds][i]].end();
}
}
for(int i=0;i<U;++i){
for(int j=0;j<siz;++j) if(i>>j&1){
int t=G[nds][j];
fa[j]=j;
if(ini[t]){
if(!col[nds][i]) col[nds][i]=ini[t];
else if(col[nds][i]!=ini[t]) col[nds][i]=-1;
}
}
for(int j=0;j<siz;++j) if(i>>j&1){
for(int k=j+1;k<siz;++k) if(i>>k&1){
if(exi[j][k]) fa[find(j)]=find(k);
}
}
for(int j=0;j<siz;++j) if(i>>j&1) con[nds][i]+=fa[j]==j;
}
}
}else ckmin(low[x],dfn[t]);
}
return ;
}
inline int solve(int x,int fat,int c){
if(~dp[x][c]) return dp[x][c];
if(x<=n){
int sum=0;
for(auto t:G[x]) if(t!=fat) sum+=solve(t,x,c);
return dp[x][c]=sum;
}
if(ini[fat]&&ini[fat]!=c) return dp[x][c]=inf;
int num=G[x].size(),U=1<<num; --U;
int dp1[64],dp2[64][25];
rep(i,0,k) dp2[0][i]=0;
for(int i=1;i<=U;++i){
dp2[i][0]=inf;
int lb=i&(-i),id=__builtin_ctzll(lb);
for(int c=1;c<=k;++c){
if(G[x][id]==fat) dp2[i][c]=dp2[i^lb][c];
else dp2[i][c]=dp2[i^lb][c]+solve(G[x][id],x,c);
ckmin(dp2[i][0],dp2[i][c]);
}
}
dp1[0]=0;
for(int S=1;S<=U;++S){
dp1[S]=inf;
for(int T=S;T;T=(T-1)&S){
if(col[x][T]==-1) continue;
int cur=col[x][T];
if(T&1){
if(cur!=c&&cur) continue;
cur=c;
}
ckmin(dp1[S],dp1[S^T]+con[x][T]+dp2[T][cur]);
}
}
return dp[x][c]=dp1[U];
}
signed main(){
freopen("colorful.in","r",stdin); freopen("colorful.out","w",stdout);
nds=n=read(); m=read(); k=read(); read();
for(int i=1;i<=n;++i) ini[i]=read();
for(int i=1;i<=m;++i){
int u=read(),v=read();
edge[u].insert(v);
edge[v].insert(u);
g[u].emplace_back(v);
g[v].emplace_back(u);
}
for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
memset(dp,-1,sizeof(dp));
int val=inf;
if(ini[1]) val=solve(1,0,ini[1]);
else rep(i,1,k) ckmin(val,solve(1,0,i));
print(val-(nds-n)+1);
return 0;
}
逆轉函式
預處理每個 \(i\) 的上一次下一次的出現位置 \(pre_i,nxt_i\),那麼 \([l,r]\) 存在逆轉函式等價於 \(r-l\le 1\) 或者以下三個條件都滿足:
-
\([l+1,r-1]\) 存在逆轉函式
-
\(pre_r\le l\) 或者 \(a_l=a_{l+r-pre_r}\)
-
\(nxt_l\ge r\) 或者 \(a_r=a_{l+r-nxt_r}\)
注意到這種判定方式和迴文串類似,而所求是每個 “類迴文區間” 的 \(m^{m-n(l,r)}\) 的和,用 \(\rm Manacher\) 來做,計算每個點作為迴文中心時對答案的貢獻
將 \([l,r]\) 擴充套件成 \([l-1,r+1]\) 時維護 \(n(l,r)\) 以及新增的對答案的貢獻是平凡的,但是在繼承時存在兩者取 \(\min\) 的操作會讓資訊減少,那麼先繼承 \(2i-mid\) 的資訊,再減至 mid+r[mid]
處,資訊的減少和增加的處理方式一樣
觀察到如果保留最靠右的 \(i+r_i\) 的 \(i\) 則 \(i-r_i\) 是不斷右移的,而擴充套件本身會讓最靠右的迴文串的右端點右移,那麼複雜度就是 \(\Theta(n)\) 的
Code Display
const int N=6e5+10;
int a[N],pw[N],n,m;
int pre[N],lst[N],nxt[N];
int num[N],sum[N],r[N];
signed main(){
freopen("invfunc.in","r",stdin); freopen("invfunc.out","w",stdout);
n=read(); m=read();
for(int i=1;i<=n;++i) a[i]=read();
for(int i=n;i>=1;--i) a[i<<1]=a[i];
rep(i,1,n*2+1) if(i&1) a[i]=m+1;
pw[0]=1;
for(int i=1;i<=n*2+1;++i){
pw[i]=mul(pw[i-1],m);
pre[i]=lst[a[i]];
lst[a[i]]=i;
}
for(int i=1;i<=m+1;++i) lst[i]=n*2+2;
for(int i=n*2+1;i>=1;--i){
nxt[i]=lst[a[i]];
lst[a[i]]=i;
}
auto check=[&](int l,int r){
if(r-l<=1) return true;
if(!l||r>2*n+1) return false;
if(pre[r]>l&&a[l]!=a[l+r-pre[r]]) return false;
if(nxt[l]<r&&a[r]!=a[l+r-nxt[l]]) return false;
return true;
};
int ans=0,cen=num[1]=1;
for(int i=2;i<=n*2;++i){
if(i<=cen+r[cen]){
r[i]=r[2*cen-i];
sum[i]=sum[2*cen-i];
num[i]=num[2*cen-i];
while(i+r[i]>cen+r[cen]){
if((i+r[i])&1) ckdel(sum[i],pw[m+1-num[i]]);
int lef=cen*2-i-r[i],rig=cen*2-i+r[i];
if(nxt[lef]>rig) --num[i];
if(pre[rig]<lef+1) --num[i];
--r[i];
}
}else num[i]=1;
while(check(i-r[i]-1,i+r[i]+1)){
++r[i];
if(nxt[i-r[i]]>i+r[i]-1) ++num[i];
if(pre[i+r[i]]<i-r[i]) ++num[i];
if((i+r[i])&1) ckadd(sum[i],pw[m+1-num[i]]);
}
if(i+r[i]>=cen+r[cen]) cen=i;
ckadd(ans,sum[i]);
}
print(ans);
return 0;
}