多校联训图论专题
【UNR #5】获奖名单
【UER #9】知识网络
考虑为何复杂度低于 \(n\) 次最短路算法——部分点具有类似信息。大概是同颜色点。那么可以预处理每种颜色的点到每个点的最短路。
然后考虑 \(a → b\) 实际最短路。它与 \(\text{color(a)}\to \text{b}\) 相去不远。仔细想想,就是将 \(\text{color(a)}\) 全体点设为同一个 \(\text{dis}\) 时,若 \(a\) 能够转移到 \(b\) 则 \(a\to b\) 实际最短路会少 \(1\) 。
于是变为最短路图上可达性问题,\(O[\frac{n(n+m)}{ω}+k(n+m)]\) 。本题卡空间,需分段进行 \(\text{bitset}\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int cnt[1<<16];
vector<int> vec[155],e[50005];
int dis[50005],vis[50005];
unsigned long long f[50005],tg[155];
long long res[305];
int n,m,k,col[50005];
void solve(int id){
if(!vec[id].size())return;
int M=(vec[id].size()-1)/64;
for(int i=1;i<=n;i++)dis[i]=-1;
for(int i=1;i<=k;i++)vis[i]=-1;
vis[id]=0;
queue<int> q;
for(auto it:vec[id]){
q.push(it);dis[it]=2;
}
vector<int> topu;
while(!q.empty()){
int x=q.front(),b=col[x];q.pop();topu.push_back(x);
if(vis[b]==-1){
vis[b]=dis[x]+1;
for(auto u:vec[b])if(dis[u]==-1)dis[u]=dis[x]+1,q.push(u);
}
for(auto u:e[x]){
if(dis[u]==-1)dis[u]=dis[x]+1,q.push(u);
}
}
for(int i=0;i<=M;i++){
for(int j=1;j<=n;j++)f[j]=0;
for(int j=1;j<=k;j++)tg[j]=0;
int l=i*64,r=(i==M?vec[id].size()-1:l+63);
for(int j=l;j<=r;j++)f[vec[id][j]]|=1ull<<(j-l);
for(auto x:topu){
int b=col[x];
if(dis[x]==vis[b])f[x]|=tg[b];
if(vis[b]==dis[x]+1)tg[b]|=f[x];
for(auto u:e[x])if(dis[u]==dis[x]+1)f[u]|=f[x];
}
int S=r-l+1;
for(int j=1;j<=n;j++){
if(dis[j]==-1)res[2*k+1]+=S;
else{
int S1=cnt[f[j]&65535];
S1+=cnt[(f[j]>>16)&65535];
S1+=cnt[(f[j]>>32)&65535];
S1+=cnt[(f[j]>>48)&65535];
res[dis[j]-1]+=S1;
res[dis[j]]+=S-S1;
}
}
}
}
int main(){
cnt[0]=0;
for(int i=1;i<(1<<16);i++)cnt[i]=cnt[i/2]+(i&1);
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)scanf("%d",&col[i]);
for(int i=1;i<=n;i++)vec[col[i]].push_back(i);
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
e[x].push_back(y);e[y].push_back(x);
}
for(int i=1;i<=k;i++)solve(i);
res[1]=0;
for(int i=1;i<=2*k+1;i++)printf("%lld ",res[i]/2);
return 0;
}
【UR #2】跳蚤公路
不难发现要求的就是是否存在负环。也就是我们只需要找到所有的负的简单环,很容易就可以想到维护路径上和 \(x\) 相关的内容,即维护一下 \(u\) 到 \(v\) 路径上,含有 \(kx\) 的路径的最小的 \(b\) 。这个可以用 \(\text{Floyd}\) 在 \(O(n^5)\) 的复杂度中求解。这样子我们用 \(f[u][u][k]\) 就知道了一个包含了 \(u\) 的,且 \(x\) 系数为 \(k\) 的最小的环,求出其负环的值域范围,接着其能够到达的所有点都会收到这个负环的限制。
我们的主要目的是找出简单负环,找负环可以用 \(\text{Bellman−Ford}\) 算法,设 \(f[t][v]\) 表示从 \(1\) 号点开始走不超过 \(t\) 步,到达 \(v\) 的最短路。因为最短路是简单路径,不会有重复点,所谓 \(f[t−1][v]=f[t][v]\) 。否则如果不等必定存在负环。
注意这个只能检测负环是否存在,我们还要考虑负环会造成的影响,如果一个点 \(f[t][v]<f[t−1][v]\),证明这个点受到了负环的影响,同理其所有能够到达的点也能够受到负环的影响。
那么我们把这个想法拓展一步,变为 \(f[t][v][k]\) ,表示走了 \(t\) 步,到 \(v\) ,\(x\) 的系数为 \(k\) 的最短路。
那么如果存在负环,就要满足:
那么只需要把这个数组搞出来,然后就可以解x范围了。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const long long INF=4e18;
struct Edge{
int to,nxt;
}Graph[10005];
vector<pair<long long,long long> > R[105],proc;
bool reach[105][105];
long long lef[105],rig[105];
long long dp[2][105][205];
int fr[10005],to[10005],coe[10005],wei[10005];
int n,m;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&fr[i],&to[i],&wei[i],&coe[i]);
}
int pre=1,nxt=0;
for(int i=1;i<=n;i++){
for(int j=-n;j<=n;j++)dp[pre][i][j+n]=dp[nxt][i][j+n]=INF;
}
dp[nxt][1][n]=0;
for(int i=1;i<=n;i++){
pre^=1,nxt^=1;
for(int j=1;j<=n;j++){
for(int k=-n;k<=n;k++)dp[nxt][j][k+n]=dp[pre][j][k+n];
}
for(int j=1;j<=m;j++){
int u=fr[j],v=to[j],c=coe[j],w=wei[j];
for(int k=-i+1;k<=i-1;k++)dp[nxt][v][k+c+n]=min(dp[nxt][v][k+c+n],dp[pre][u][k+n]+w);
}
}
for(int i=1;i<=m;i++)reach[fr[i]][to[i]]=1;
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++)if(i!=k){
for(int j=1;j<=n;j++)if(i!=j&&j!=k)reach[i][j]|=reach[i][k]&reach[k][j];
}
}
for(int i=1;i<=n;i++){
for(int k=-n;k<=n;k++){
if(dp[nxt][i][k+n]>1e15)continue;
long long tmpL=-INF,tmpR=INF;
for(int j=-n;j<=n;j++){
if(dp[pre][i][j+n]>1e15)continue;
if(j<k)
tmpR=min(tmpR,(long long)ceil(1.0*(dp[pre][i][j+n]-dp[nxt][i][k+n])/(k-j)));
if(j==k){
if(dp[pre][i][j+n]<=dp[nxt][i][k+n]){
tmpL=INF,tmpR=-INF;
break;
}
}
if(j>k)tmpL=max(tmpL,(long long)floor(1.0*(dp[pre][i][j+n]-dp[nxt][i][k+n])/(k-j)));
}
if(tmpL<tmpR)R[i].push_back({tmpL,tmpR});
}
}
for(int i=1;i<=n;i++){
proc.clear();
for(int j=1;j<=n;j++){
if(reach[j][i]||i==j){
for(auto it:R[j])proc.push_back(it);
}
}
long long l=INF,r=-INF,lst=-INF;
sort(proc.begin(),proc.end());
for(int k=0;k<proc.size();k++){
if(!k&&proc[k].first>-INF){
l=-INF,r=proc[k].first;
break;
}
if(lst>-INF&&proc[k].first>=lst){
l=lst,r=proc[k].first;
break;
}
lst=max(lst,proc[k].second);
}
if(r==-INF&&lst<INF)l=lst,r=INF;
if(proc.empty()||l==-INF||r==INF)puts("-1");
else printf("%lld\n",max(0ll,r-l+1));
}
return 0;
}
【UNR #4】同构判定鸭
最短的合法串长度 \(≤n_1+n_2\) 。
考虑统计串 \(s=s_1s_2⋯s_L\) 在 \(G_1,G_2\) 中的出现次数之差:将两个图拼成 \(n_1+n_2\) 个点的大图 \(G\),设 \(f_{k,v}\) 表示长度为 \(k\) 且与 \(s_1⋯s_k\) 匹配且最后一个节点是 \(v\) 的路径数量。
这是线性递推,可以写成矩阵形式:设 \(26\) 个字符的邻接矩阵是 \(M_a,⋯,M_z\),\(u^⊤_I\) 是元素均为 \(1\) 的行向量,\(u_O\) 是前 \(n_1\) 个元素为 \(1\),后 \(n_2\) 个元素为 \(−1\) 的列向量,则串合法当且仅当 \(u^⊤_IM_{s_1}⋯M_{s_L}u_O≠0\) 。
设 \(V_K\) 表示所有 \(L≤K\) 的字符串 \(s\) 对应的行向量 \(u^⊤_IM_{s_1}M_{s_2}⋯M_{s_L}\) 的集合,则最短的合法串长度 \(≤K\) 当且仅当 \(V_K\) 中有元素 \(u_⊤\) 满足 \(u^⊤u_O≠0\)。
设 \(U_K=\text{span(}V_K\text{)}\),则又转化为 \(U_K\) 中有元素 \(u^⊤\) 满足 \(u^⊤u_O≠0\) 。
对于行向量集合 \(V\) 和矩阵 \(M\),设 \(VM=\{u^⊤M:u∈V\}\),则:
显然 \(U_1⊆U_2⊆⋯\),且因为它是一阶递推式,而维数 \(≤n1+n2\),所以 \(U_{n1+n2}=U_{n1+n2+1}=⋯\),所以若 \(U_K\) 内有元素 \(u^⊤u_O≠0\) ,则最小的 \(K≤n_1+n_2\) ,得证。
先把长度求出来,最后按位贪心即可,时间复杂度 \(O(nm|Σ|)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const unsigned long long md=998244353;
unsigned long long hsh[1005][26];
struct mat{
int n,m;
unsigned long long a[505][1005],f[505],g[505];
vector<pair<int,int> > E[505];
mat(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;char c[3];scanf("%d%d%s",&x,&y,c);
E[x].push_back(make_pair(y,c[0]-'a'));
}
for(int i=1;i<=n;i++)a[i][0]=g[i]=1;
}
inline unsigned long long solve(int loc){
unsigned long long res=0;
for(int i=1;i<=n;i++){
for(auto it:E[i])(a[i][loc]+=a[it.first][loc-1]*hsh[loc][it.second])%=md;
(res+=a[i][loc])%=md;
}
return res;
}
inline unsigned long long Get(int c,int loc){
unsigned long long res=0;
for(int i=1;i<=n;i++){
for(auto it:E[i])if(it.second==c)(res+=g[i]*hsh[loc][c]%md*a[it.first][loc-1])%=md;
}
return res;
}
inline void update(int c,int loc){
for(int i=1;i<=n;i++){
for(auto it:E[i])if(it.second==c)(f[it.first]+=g[i]*hsh[loc][c])%=md;
}
for(int i=1;i<=n;i++)g[i]=f[i];
for(int i=1;i<=n;i++)f[i]=0;
}
}G1,G2;
int main(){
int L=G1.n+G2.n;srand(time(0));
for(int i=1;i<=L;i++){
for(int j=0;j<26;j++)hsh[i][j]=1ll*rand()*rand()*rand()+1ll*rand()*rand()+rand();
for(int j=0;j<26;j++)hsh[i][j]%=md;
}
for(int i=1;i<=L;i++){
if(G1.solve(i)!=G2.solve(i)){
vector<char> tmp;
for(int j=i;j;j--){
for(int t=0;t<26;t++){
if(G1.Get(t,j)!=G2.Get(t,j)){
putchar(t+'a');G1.update(t,j);G2.update(t,j);
break;
}
}
}
return 0;
}
}
puts("Same");
return 0;
}
新年的Dog划分
考虑建出原图的一棵生成树,对树黑白染色即可。
考虑对每个点,依次二分出所有必须要连的边,其余的边则删掉。得到生成树后染色,还要检查图是否存在二分图,也就是检查是否有连接同色点的边。具体方法就是删除所有不在树上的链接异色点的边,然后枚举树上的每条边,检查是否删去某条边后图依然联通。
点击查看代码
#include<bits/stdc++.h>
#include "graph.h"
using namespace std;
int n;
int ver[405],ne[405],head[205],cnt;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
bool col[205],vis[205][205];
void dfs(int x,int fi,int op){
col[x]=op;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x,op^1);
}
}
vector<int> check_bipartite(int vsize){
n=vsize;
vector<pair<int,int> > vec,LINK;
for(int i=0;i<n;i++){
int lim=i+1;
while(lim<n){
for(int j=lim;j<n;j++)vec.push_back(make_pair(i,j));
if(query(vec))break;
for(int j=lim;j<n;j++)vec.pop_back();
int l=lim,r=n-1;
while(l<r){
int mid=(l+r)>>1;
for(int j=l;j<=mid;j++)vec.push_back(make_pair(i,j));
if(query(vec))l=mid+1;
else r=mid;
for(int j=l;j<=mid;j++)vec.pop_back();
}
link(i,l);link(l,i);LINK.push_back(make_pair(i,l));
lim=l+1;vis[i][l]=1;
}
}
dfs(1,1,1);vec.clear();
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++)if(col[i]!=col[j]&&!vis[i][j])vec.push_back(make_pair(i,j));
}
for(auto it:LINK){
vec.push_back(it);
if(query(vec))return vector<int>();
vec.pop_back();
}
vector<int> ans;
for(int i=0;i<n;i++)if(col[i])ans.push_back(i);
return ans;
}
CF1264E Beautiful League
题目要求最大化 \(A\to B\to C\to A\) 这样的 “三元环” 的个数,考虑不合法的三元组一定有且仅有一个出度为 \(2\) 的点,考虑在这个点处统计不合法方案,答案即为 \(\frac{n\times(n-1)\times(n-2)}{6}-\sum_{i=1}^n {deg_i\choose 2}\) 。
给每条边定向,最小化 \(\sum_{i=1}^n {deg_i\choose 2}\) ,跑费用流即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int ver[100005],ne[100005],head[10005],tot=1,val[100005],cost[100005];
inline void link(int x,int y,int v,int c){
ver[++tot]=y;
ne[tot]=head[x];
head[x]=tot;val[tot]=v;
}
int dis[10005],s,t,cnt;
queue<int> q;
bool vis[10005];
inline bool bfs(){
for(int i=1;i<=cnt;i++)dis[i]=1e9;
dis[t]=0;q.push(t);vis[t]=1;
while(!q.empty()){
int x=q.front();q.pop();vis[x]=0;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(val[i^1]&&dis[u]>dis[x]+cost[i]){
dis[u]=dis[x]+cost[i];
if(!vis[u])q.push(u);vis[u]=1;
}
}
}
return dis[s]!=1e9;
}
int dfs(int x,int cap){
if(x==t||!cap)return cap;
int res=0;vis[x]=1;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(vis[u]||dis[u]!=dis[x]+cost[i])continue;
int tmp=dfs(u,min(val[i],cap));
cap-=tmp;res+=tmp;val[i]-=tmp;val[i^1]+=tmp;
}
if(!res)dis[x]=1e9;vis[x]=0;
return res;
}
inline int water(){
int res=0;
while(bfs())res+=dfs(s,1e9);
return res;
}
bool tag[55][55];
int mp[55],id[55][55],ans[55][55],loc[55],out[55];
int main(){
scanf("%d%d",&n,&m);
s=++cnt;t=++cnt;
for(int i=1;i<=n;i++){
mp[i]=++cnt;
for(int j=i+1;j<=n;j++){
id[i][j]=id[j][i]=++cnt;
link(s,id[i][j],1,0);link(id[i][j],s,0,0);
}
link(mp[i],t,1,-1);link(t,mp[i],0,1);loc[i]=1;out[i]=tot;
}
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
tag[x][y]=tag[y][x]=1;
link(id[x][y],mp[x],1,0);link(mp[x],id[x][y],0,0);ans[x][y]=tot;
}
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(tag[i][j])continue;
link(id[i][j],mp[i],1,0);link(mp[i],id[i][j],0,0);ans[i][j]=tot;
link(id[i][j],mp[j],1,0);link(mp[j],id[i][j],0,0);ans[j][i]=tot;
}
}
int res=0;
while(res!=n*(n-1)/2){
res+=water();
for(int i=1;i<=n;i++){
if(val[out[i]]){
link(mp[i],t,1,loc[i]*loc[i]-(loc[i]+1)*(loc[i]+1));
link(t,mp[i],0,(loc[i]+1)*(loc[i]+1)-loc[i]*loc[i]);loc[i]++;out[i]=tot;
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)printf("%d",val[ans[i][j]]);
puts("");
}
return 0;
}
CF1239E Turtle
可以发现最优摆放方式一定是最小值和次小值一个放左上角一个放右下角,上面升序排列,下面倒序排列。最优行走路线要么将上面一行走完,要么将下面一行走完。
背包算出将最小值和次小值去除后的所有可能,取最优结果即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[55],sum;
bitset<1250005> dp[25];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)scanf("%d",&a[i+n]);
sort(a+1,a+2*n+1);
dp[0][0]=1;
for(int i=3;i<=2*n;i++){
sum+=a[i];
for(int j=n-1;j;j--)dp[j]|=(dp[j-1]<<a[i]);
}
int ans=1e9,up=0;
for(int i=0;i<=sum;i++)if(dp[n-1][i]){
if(ans>max(a[1]+a[2]+i,a[1]+a[2]+sum-i)){
ans=max(a[1]+a[2]+i,a[1]+a[2]+sum-i);
up=i;
}
}
vector<int> UP,DOWN;
int tot=n-1;
for(int i=3;i<=2*n;i++){
if(tot&&dp[tot-1][up-a[i]]){
UP.push_back(a[i]);
up-=a[i];tot--;
}else DOWN.push_back(a[i]);
}
sort(UP.begin(),UP.end());reverse(UP.begin(),UP.end());
sort(DOWN.begin(),DOWN.end());
printf("%d ",a[1]);
for(auto it:DOWN)printf("%d ",it);puts("");
for(auto it:UP)printf("%d ",it);
printf("%d",a[2]);
return 0;
}
CF521E Cycling City
存在两点之间有三条路径等价于存在两个环有公共边,考虑建出一棵生成树,每条非树边会产生一个环,暴力染色检查是否有相交即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int ver[400005],ne[400005],head[200005],cnt;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
int dsu[200005];
int find(int x){
return dsu[x]==x?x:dsu[x]=find(dsu[x]);
}
int a[200005],b[200005],top;
int dep[200005],fa[200005];
void dfs(int x,int fi){
dep[x]=dep[fi]+1;fa[x]=fi;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x);
}
}
int col[200005];
inline int lca(int x,int y,int i){
while(x!=y){
if(col[x])return col[x];
if(col[y])return col[y];
if(dep[x]>=dep[y])col[x]=i,x=fa[x];
else col[y]=i,y=fa[y];
}
if(col[x])return col[x];col[x]=i;
return 0;
}
inline vector<int> Get(int x,int y){
vector<int> L,R;
while(x!=y){
if(dep[x]>=dep[y])L.push_back(x),x=fa[x];
else R.push_back(y),y=fa[y];
}
L.push_back(x);while(!R.empty())L.push_back(R.back()),R.pop_back();
return L;
}
inline vector<int> operator +(vector<int> a,vector<int> b){
for(auto it:b)a.push_back(it);
return a;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)dsu[i]=i;
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
if(find(x)==find(y))++top,a[top]=x,b[top]=y;
else {
link(x,y);link(y,x);
dsu[find(x)]=find(y);
}
}
dfs(1,1);
for(int i=1;i<=top;i++){
int tmp=lca(a[i],b[i],i);
if(!tmp)continue;
puts("YES");
vector<int> L=Get(a[i],b[i]),R=Get(a[tmp],b[tmp]);
set<int> s;for(auto it:L)s.insert(it);
vector<int> ans;
for(auto it:R)if(s.count(it))ans.push_back(it);
printf("%ld ",ans.size());for(auto it:ans)printf("%d ",it);puts("");
{
auto ll=Get(ans[0],a[i])+Get(b[i],ans.back()),rr=Get(ans[0],b[i])+Get(a[i],ans.back());
if(ll.size()>rr.size())swap(ll,rr);
printf("%ld ",ll.size());for(auto it:ll)printf("%d ",it);
puts("");
}
{
auto ll=Get(ans[0],a[tmp])+Get(b[tmp],ans.back()),rr=Get(ans[0],b[tmp])+Get(a[tmp],ans.back());
if(ll.size()>rr.size())swap(ll,rr);
printf("%ld ",ll.size());for(auto it:ll)printf("%d ",it);
puts("");
}
return 0;
}
puts("NO");
return 0;
}
CF1142E Pink Floyd
考虑没有粉色边的情况,我们可以把所有点加入一个集合里,随便拿出两个点 \(a,b\),询问它们之间的边的方向,假如 \(a\to b\) ,发现 \(b\) 能到达的点 \(a\) 也能到达,因此可以把 \(b\) 删掉,操作 \(n-1\) 次后得到的点即可作为答案。
考虑有粉色边的情况,如果还是随便拿两个点,这两个点之间可能已经有边了,而这条边又不能满足上面的性质,我们删去一些,使之成为一个 \(\text{DAG}\) ,在集合中储存入度数为 \(0\) 的点,容易发现这样不会有两个有边相连的点同时在集合中,每次删点时将新产生的入度为 \(0\) 的点加入,操作 \(n-1\) 次得到答案。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int ver[100005],ne[100005],head[100005],cnt,deg[100005];
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
vector<int> vec[100005];
bool vis[100005],ins[100005];
void dfs(int x){
vis[x]=ins[x]=1;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(!ins[u])vec[x].push_back(u),++deg[u];
if(!vis[u])dfs(u);
}
ins[x]=0;
}
inline void query(int &x,int &y){
printf("? %d %d\n",x,y);fflush(stdout);
int T;scanf("%d",&T);
if(!T)swap(x,y);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
link(x,y);
}
for(int i=1;i<=n;i++)if(!vis[i])dfs(i);
vector<int> tmp;
for(int i=1;i<=n;i++)if(!deg[i])tmp.push_back(i);
while(tmp.size()>1){
int x=tmp.back();tmp.pop_back();
int y=tmp.back();tmp.pop_back();
query(x,y);tmp.push_back(x);
for(auto u:vec[y])if(!--deg[u])tmp.push_back(u);
}
printf("! %d\n",tmp[0]);
return 0;
}
CF611H New Year and Forgotten Tree
将点按照不同的位数归类,枚举所有位数的集合,容易发现无解当且仅当某个集合内的边数大于等于点数,这个判断的复杂度时 \(O(2^6)\) 很小,同时连边的方式又只有 \(6\times 6=36\) 种,暴力枚举连边方式暴力 \(\text{check}\) 即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int E[10][10],bas[10],cnt[200005],sum[10];
inline bool check(){
for(int s=1;s<(1<<cnt[n]);s++){
int sume=0,sumn=0;
for(int i=0;i<cnt[n];i++){
if((s>>i)&1){
sumn+=sum[i];
for(int j=0;j<cnt[n];j++)if((s>>j)&1)sume+=E[i][j];
}
}
if(sume>=sumn)return 0;
}
return 1;
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;i++){
string a,b;cin>>a>>b;
int x=a.length(),y=b.length();
E[x-1][y-1]++;
}
for(int i=1;i<=n;i++)cnt[i]=cnt[i/10]+1;
for(int i=1;i<=n;i++)sum[cnt[i]-1]++;
bas[0]=1;for(int i=1;i<cnt[n];i++)bas[i]=bas[i-1]*10;
if(!check()){
puts("-1");
return 0;
}
bool flag=1;
while(flag){
flag=0;
for(int i=0;!flag&&i<cnt[n];i++){
for(int j=0;j<cnt[n];j++){
if(E[i][j]){
if(sum[i]){
sum[i]--;E[i][j]--;
if(check()){
printf("%d %d\n",bas[i]+sum[i],bas[j]);
flag=1;break;
}
sum[i]++;E[i][j]++;
}
if(sum[j]){
sum[j]--;E[i][j]--;
if(check()){
printf("%d %d\n",bas[i],bas[j]+sum[j]);
flag=1;break;
}
sum[j]++;E[i][j]++;
}
}
}
}
}
for(int i=0;i<cnt[n];i++){
for(int j=0;j<cnt[n];j++){
if(E[i][j])printf("%d %d\n",bas[i]+sum[i]-1,bas[j]);
}
}
return 0;
}
CF627F Island Puzzle
容易发现在不加边的情况下,最终形成的局面是唯一的,将 \(0\) 号点移动到对应位置后比较整棵树是否符合目标状态即可。
考虑加入一条边后能带来的影响,发现它会在原本的树上形成一个环,可以通过移动使得环上除了最靠近终点的节点以外的点旋转一个距离。
比较是否可行,并统计答案即可,当环在 \(0\) 节点到终点的路径上时,还要考虑旋转方向带来的影响。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[200005],b[200005];
int ver[400005],ne[400005],head[200005],cnt;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
int dep[200005],siz[200005],fa[200005],son[200005];
void dfs1(int x,int fi){
siz[x]=1;fa[x]=fi;dep[x]=dep[fi]+1;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs1(u,x);siz[x]+=siz[u];
if(siz[u]>siz[son[x]])son[x]=u;
}
}
int top[200005];
void dfs2(int x,int fi){
top[x]=fi;
if(son[x])dfs2(son[x],fi);
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fa[x]||u==son[x])continue;
dfs2(u,u);
}
}
inline int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]>=dep[top[y]])x=fa[top[x]];
else y=fa[top[y]];
}
return dep[x]<dep[y]?x:y;
}
inline int dist(int x,int y){
int lc=lca(x,y);
return dep[x]+dep[y]-2*dep[lc];
}
inline bool under(int x,int y){
return lca(x,y)==x;
}
int near[200005],dif[200005],mp[200005],col[200005];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)scanf("%d",&b[i]);
for(int i=1;i<n;i++){
int x,y;scanf("%d%d",&x,&y);
link(x,y);link(y,x);
}
int rt=0;
for(int i=1;i<=n;i++)if(!b[i])rt=i;
dfs1(rt,0);dfs2(rt,rt);
int s=0;
for(int i=1;i<=n;i++)if(!a[i])s=i;
for(int i=s;i;i=fa[i])swap(a[i],a[fa[i]]),col[i]++;
bool flag=1;
for(int i=1;i<=n;i++)if(a[i]!=b[i])flag=0;
if(flag){
printf("0 %d\n",dist(s,rt));
}
else {
// for(int i=1;i<=n;i++)cout<<a[i]<<" "<<b[i]<<endl;
vector<int> vec;
for(int i=1;i<=n;i++)if(a[i]!=b[i])vec.push_back(i);
for(auto it:vec)dif[it]=1;
for(auto it:vec){
for(int i=head[it];i;i=ne[i]){
int u=ver[i];
near[u]++;
}
}
vector<int> ed;int tot=0,low=0;
for(int i=1;i<=n;i++)if(near[i]==1&&dif[i])ed.push_back(i);
for(int i=1;i<=n;i++)if(!near[i]&&dif[i])ed.push_back(i),tot++;
int delta=0;
for(auto it:vec){
delta+=col[it];
if(col[it]&&dep[low]<dep[it])low=it;
}
delta=max(0,2*delta);
if(ed.size()+tot==2){
int p=(dist(ed[0],rt)<dist(ed[1],rt)?ed[0]:ed[1]);
if(p!=ed[1])reverse(ed.begin(),ed.end());p=fa[p];
int len=vec.size();
for(auto it:vec)mp[b[it]]=it;
int tmp=-1;
for(auto it:vec){
int dis=dist(it,mp[a[it]]);dis=min(dis,len-dis);
if(~tmp&&tmp!=dis){
puts("-1");
return 0;
}else tmp=dis;
}
vector<int> ans;
if(low){
int dis=dist(low,mp[a[low]]);
if(dis>len-dis)delta=0;
}
printf("%d %d %lld\n",min(ed[0],p),max(ed[0],p),dist(s,p)+dist(p,rt)+1ll*tmp*(len+1)-delta);
}
else if(ed.size()+tot==4){
int p=0;
for(int i=1;i<=n;i++)if(!dif[i]&&near[i]==2)p=i;
if(!p){
puts("-1");
return 0;
}
int len=vec.size();
for(auto it:vec)mp[b[it]]=it;
int tmp=-1;
for(auto it:vec){
int dis=dist(it,mp[a[it]]);
if(dist(it,p)+dist(p,mp[a[it]])==dis)dis--;dis=min(dis,len-dis);
if(~tmp&&tmp!=dis){
puts("-1");
return 0;
}else tmp=dis;
}
vector<int> ans;
for(int i=1;i<=n;i++)if(!near[i]&&dif[i])ans.push_back(i);
for(auto it:ed)if(dist(it,p)!=1)ans.push_back(it);sort(ans.begin(),ans.end());
if(low){
int dis=dist(low,mp[a[low]]);
if(dist(low,p)+dist(p,mp[a[low]])==dis)dis--;
if((dis>len-dis)^under(low,mp[a[low]]))delta=0;
}
printf("%d %d %lld\n",ans[0],ans[1],dist(s,p)+dist(p,rt)+1ll*tmp*(len+1)-delta);
}
else {
puts("-1");
return 0;
}
}
return 0;
}
CF1305G Kuroni and Antihype
假如一个 \(0\) 号点,使得每一个点都有父亲节点,将边权更改为两个点的边权之和,最后将每个点的点权减去一次就好了。
可以从大到小枚举边权,暴力枚举子集,时间复杂度 \(O(3 ^{18} \alpha (n))\) ,可以通过。
也可以考虑一个叫 \(\text{Borůvka}\) 的算法,通过 \(\text{FMT}\) 计算每一个点集合的最大和次大出边,每轮选择最大的连向集合外的出边合并,进行 \(\log n\) 轮,时间复杂度 \(O(a\log a\log n)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,mx,lim,a[300005],dsu[300005],cnt[300005];
long long ans;
inline int find(int x){
return dsu[x]==x?x:dsu[x]=find(dsu[x]);
}
inline void merge(int u,int v,long long w){
if(cnt[u]&&cnt[v]){
u=find(u),v=find(v);
if(u!=v){
ans+=w*(cnt[u]+cnt[v]-1);
dsu[u]=v;cnt[v]=1;
}
}
}
int main(){
scanf("%d",&n);cnt[0]=1;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);cnt[a[i]]++;
ans-=a[i];
mx=max(mx,a[i]);
}
int lim=1;while(lim<=mx)lim<<=1;
for(int i=0;i<lim;i++)dsu[i]=i;
for(int i=lim-1;~i;i--){
for(int j=i;j;j=(j-1)&i)if(cnt[j]&&cnt[i^j])merge(j,i^j,i);
merge(i,0,i);
}
printf("%lld\n",ans);
return 0;
}
[ARC103D] Distance Sums
显然,到所有点距离最小的点是整棵树的重心,将其设为根,发现若点 \(x\) 的父亲为 \(fa\) ,有:\(D_{fa}=D_x+siz_x-(n-siz_x)\) ,即 \(D_{fa}=D_x+2\times siz_x-n\) ,逐层剥叶子即可,如果有某个点找不到父亲,说明无解。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
long long D[100005];
int siz[100005],fa[100005];
map<int,int> mp;
inline bool cmp(int x,int y){
return D[x]>D[y];
}
int ver[100005],ne[100005],head[100005],cnt;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
long long sum;
void dfs(int x,int dep){
sum+=dep;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
dfs(u,dep+1);
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%lld",&D[i]);
vector<int> vec;
for(int i=1;i<=n;i++)vec.push_back(i);
sort(vec.begin(),vec.end(),cmp);
vec.pop_back();
for(int i=1;i<=n;i++)mp[D[i]]=i;
for(auto it:vec){
siz[it]++;
long long tmpD=D[it]+2*siz[it]-n;
if(!mp.count(tmpD)){
puts("-1");
return 0;
}
else {
fa[it]=mp[tmpD];siz[mp[tmpD]]+=siz[it];
}
}
int rt=0;
for(int i=1;i<=n;i++)if(!fa[i])rt=i;
for(int i=1;i<=n;i++)if(fa[i])link(fa[i],i);
dfs(rt,0);
if(sum!=D[rt])puts("-1");
else {
for(int i=1;i<=n;i++)if(fa[i])printf("%d %d\n",i,fa[i]);
}
return 0;
}
[AGC025E] Walking on a Tree
考虑剥叶子,如果一个叶子没有路径以它为端点,直接删掉即可。如果有一条路径以它为端点,那么无论如何定向,它和父亲的连边给贡献一定是 \(1\) ,可以直接将端点改为它的父亲。如果路径数大于等于 \(2\) ,那么随便取两条路径,使这两条路径方向相反即可。
容易发现这样每条边的贡献就是经过它的路径数与 \(2\) 取 \(\text{min}\) 的值,用 \(\text{set}\) 维护以每个点为端点的路径,时间复杂度 \(O(nm \log m)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
struct node{
int tx,ty,nx,ny,res;
node(){}
node(int _tx,int _ty,int _nx,int _ny){
tx=_tx;ty=_ty;nx=_nx;ny=_ny;res=0;
}
inline void trans(int x,int y){
if(nx==x)nx=y;
if(ny==x)ny=y;
}
}E[2005];
set<int> s[2005];
int ver[4005],ne[4005],head[2005],cnt=1;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
int sum[2005];
bool find(int x,int to,int fi){
if(x==to)return 1;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
if(find(u,to,x)){
sum[i>>1]++;return 1;
}
}
return 0;
}
int dsu[2005];
bool vis[2005];
int Get(int x){
if(vis[x]||dsu[x]==x)return E[x].res;
vis[x]=1;
return E[x].res^=Get(dsu[x]);
}
void dfs(int x,int fi){
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x);
}
if(s[x].empty())return ;
else if(s[x].size()==1){
int it=*s[x].begin();
E[it].trans(x,fi);
if(E[it].nx==fi&&E[it].ny==fi)s[fi].erase(it);
else s[fi].insert(it);
}
else {
int it1=*s[x].begin();s[x].erase(it1);
int it2=*s[x].begin();s[x].erase(it2);
if(E[it1].nx!=x)swap(E[it1].nx,E[it1].ny),E[it1].res^=1;
if(E[it2].nx!=x)swap(E[it2].nx,E[it2].ny),E[it2].res^=1;
s[E[it2].ny].erase(it2);E[it1].nx=E[it2].ny;
if(E[it1].nx==E[it1].ny)s[E[it1].nx].erase(it1);
else s[E[it1].nx].insert(it1);
dsu[it2]=it1;E[it2].res^=(1^E[it1].res);
for(auto it:s[x]){
E[it].trans(x,fi);
if(E[it].nx==fi&&E[it].ny==fi)s[fi].erase(it);
else s[fi].insert(it);
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++){
int x,y;scanf("%d%d",&x,&y);
link(x,y);link(y,x);
}
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);find(x,y,x);
E[i]=node(x,y,x,y);s[x].insert(i);s[y].insert(i);
}
int ans=0;
for(int i=1;i<n;i++)ans+=(sum[i]>0)+(sum[i]>1);
printf("%d\n",ans);
dfs(1,1);
for(int i=1;i<=m;i++){
if(Get(i))swap(E[i].tx,E[i].ty);
printf("%d %d\n",E[i].tx,E[i].ty);
}
return 0;
}