[做题笔记] namespace_std 的杂题选讲
Latin Square
题目描述
解法
考虑所有操作都是整体操作,那么维护整体标记,本题的核心是考察整体操作对单点的影响。
发现 LRUD
这四个操作对于单点都是独立的,所以可以通过维护偏移量的方式做到。但是 IC
这个对于排列取逆的操作似乎不是独立的,单看每个元素的位置变化是混乱的。
我们想独立化这个操作,考虑取逆操作的另一种描述形式:三维平面上有 \(n^2\) 个坐标 \((i,j,p)\),对行取逆就是交换坐标的第二维和第三维;对列取逆就是交换坐标的第一维和第三维。
那么我们再维护表示维护交换的整体标记即可,时间复杂度 \(O(n^2+m)\)
总结
想要维护整体标记时,首先把整体操作对单点的影响独立开来。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,m,o[3],f[3],p[3],a[M][M][3],b[M][M];
char s[100005];
void work()
{
n=read();m=read();
for(int i=0;i<3;i++) f[o[i]=i]=0;
for(int i=0;i<n;i++) for(int j=0;j<n;j++)
{
a[i][j][2]=read()-1;
a[i][j][0]=i;a[i][j][1]=j;
}
scanf("%s",s+1);
for(int i=1;i<=m;i++)
{
if(s[i]=='L') f[1]--;
if(s[i]=='R') f[1]++;
if(s[i]=='U') f[0]--;
if(s[i]=='D') f[0]++;
if(s[i]=='I') swap(o[1],o[2]),swap(f[1],f[2]);
if(s[i]=='C') swap(o[0],o[2]),swap(f[0],f[2]);
}
for(int i=0;i<3;i++) f[i]=(f[i]%n+n)%n;
for(int i=0;i<n;i++) for(int j=0;j<n;j++)
{
for(int k=0;k<3;k++)
p[k]=(a[i][j][o[k]]+f[k])%n;
b[p[0]][p[1]]=p[2];
}
for(int i=0;i<n;i++,puts(""))
for(int j=0;j<n;j++)
printf("%d ",b[i][j]+1);
}
signed main()
{
T=read();
while(T--) work();
}
一流团子师傅
题目描述
解法
当时我写的 这篇文章 真是有用啊,本题所有技巧都可以从那题迁移过来。
考虑逐步增加方便利用的已知信息,一个显然的想法是依次插入每个位置,动态维护 \(m\) 个列表,满足其中每个元素都互不相同。设当前插入元素是 \(x\),那么我们应该把它插入到哪个列表中呢?
有一个暴力的想法是依次检查每个列表,询问 \(x\) 和列表中元素的并。如果这个并集中元素的最大出现次数 \(\leq 1\),说明 \(x\) 没有在这个列表中出现。询问一个集合元素最大出现次数的方法是,用 \(m\) 减去其补集的询问结果。
询问次数 \(n\cdot m^2\),有点不能接受。不妨把这个过程套上二分,我们询问 \(x\) 和 \([l,mid]\) 列表中元素的并。如果这个并集中元素的最大出现次数 \(\leq mid-l+1\),存在一个列表使得 \(x\) 没有在其中出现过。那么二分到 \(l=r\) 时,我们直接把 \(x\) 插入到列表 \(l\) 中就是合法的。
询问次数 \(n\cdot m\log m\),正好对应得上 \(5\cdot 10^4\) 的询问次数。
不过真的不要再迫害团子啦,我每次看到 JOI 的这种制作团子题目都要 ptsd。
#include "dango3.h"
#include <bits/stdc++.h>
using namespace std;
bool vis[10005];int n,m;
vector<int> v[30],t;
int qry()
{
t.clear();
for(int i=1;i<=n*m;i++)
if(!vis[i]) t.push_back(i);
for(int i=1;i<=n*m;i++) vis[i]=0;
return Query(t);
}
void work(int l,int r,int x)
{
if(l==r)
{
v[l].push_back(x);
return ;
}
int mid=(l+r)>>1;vis[x]=1;
for(int i=l;i<=mid;i++) for(int x:v[i])
vis[x]=1;
int h=m-qry();
if(h<=mid-l+1) work(l,mid,x);
else work(mid+1,r,x);
}
void Solve(int N,int M)
{
n=N;m=M;
for(int i=1;i<=n*m;i++) work(1,m,i);
for(int i=1;i<=m;i++) Answer(v[i]);
}
Tiles for Bathroom
题目描述
解法
考虑求出以 \((i,j)\) 为右下角的,切比雪夫距离第 \(x\) 小的点 \(c_{i,j,x}\)(颜色相同的保留距离最小的那个),这样定义是因为第 \(q+1\) 小的切比雪夫距离减 \(1\) 描述了向右上延伸的最大长度。
快速求出 \(c_{i,j,x}\) 需要充分利用以前计算过的信息,发现我们可以从 \(c_{i-1,j},c_{i,j-1},c_{i-1,j-1}\) 这三者归并上来(有重复的点没关系,因为相同颜色的点最后只会保留一个)
按切比雪夫距离归并排序 \(O(n^2q)\),当然暴力排序也是可以通过的。
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 1505;
#define pii pair<int,int>
#define pb push_back
#define fi first
#define se second
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,a[M][M],b[M],v[M*M];
vector<pii> c[M][M],t;
void get(int x,int y)
{
t.clear();
t.pb({1,read()});
for(auto i:c[x-1][y]) t.pb({i.fi+1,i.se});
for(auto i:c[x][y-1]) t.pb({i.fi+1,i.se});
for(auto i:c[x-1][y-1]) t.pb({i.fi+1,i.se});
sort(t.begin(),t.end());
for(auto i:t)
{
if(v[i.se] || c[x][y].size()>m) continue;
c[x][y].pb(i);v[i.se]=1;
}
for(auto i:t) v[i.se]=0;
t.clear();
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
get(i,j);
int p=min(i,j);
if(c[i][j].size()>m)
p=min(p,c[i][j][m].fi-1);
b[p]++;
}
for(int i=n;i>=1;i--) b[i]+=b[i+1];
for(int i=1;i<=n;i++) printf("%d\n",b[i]);
}
Resurrection
题目描述
解法
引理:\(G\) 能被生成,当且仅当 \(G\) 中的所有点 \(u\) 的父亲 \(f_p\) 都是 \(T\) 上的祖先,且不存在两条链 \((x,f_x)\) 和 \((y,f_y)\) 相交且不包含。必要性显然,下证充分性。
设点数 \(<n\) 的 \(G\) 都能被构造出来,我们现在证明任意点数 \(=n\) 的点 \(G\) 都可以被构造。
如果 \(n=1\),结论显然成立。
如果 \(n>1\),那么点 \(n\) 在 \(G\) 上至少有一个儿子。我们取 \(n\) 在 \(T\) 中最深的那个儿子 \(x\),那么边 \((x,fa_x)\) 肯定是 \(x\) 在 \(T\) 的子树内的边中,最晚被删除的那一条。根据条件子树内不会有点连接到 \(x\) 在 \(T\) 上的祖先。那么就划分成了两个独立的子问题,\(x\) 的子树和 \(G\) 除去 \(x\) 子树部分 分别合法,所以 \(G\) 也合法。
可以直接用这个引理计数,设 \(f_{u,i}\) 表示 \(u\) 选择完父亲之后,\(u\) 的儿子可选的 \(u\) 祖先的个数是 \(i\) 的方案数,转移:
如果 \(u=n\),初始化 \(f_{u,0}=1\);否则初始化 \(f_{u,1}=f_{u,2}...=f_{u,dep-1}=1\);最后的答案是 \(f_{n,0}\)
时间复杂度 \(O(n^2)\)
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 3005;
const int MOD = 998244353;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,f[M][M],d[M];vector<int> g[M];
void dfs(int u,int fa)
{
for(int i=(fa!=0);i<=d[u];i++) f[u][i]=1;
for(int v:g[u]) if(v^fa)
{
d[v]=d[u]+1;
dfs(v,u);
for(int i=1;i<=d[u]+1;i++)
f[v][i]=(f[v][i]+f[v][i-1])%MOD;
for(int i=0;i<=d[u];i++)
f[u][i]=f[u][i]*f[v][i+1]%MOD;
}
}
signed main()
{
n=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
g[u].push_back(v);
g[v].push_back(u);
}
dfs(n,0);
printf("%lld\n",f[n][0]);
}
Defender of Childhood Dreams
题目描述
解法
话说这种连必要性都要归纳的构造题怎么做啊?感觉没有任何切入点啊。
颜色数的下界是 \(\lceil \log_k n\rceil\),首先证明必要性,假设 \(n\) 个点的图有 \(c\) 染色的方法,我们尝试证明一定有 \(n\leq k^c\),即可得到上述结论,我们对 \(c\) 进行归纳。
如果 \(c=1\),上述结论显然成立。
如果 \(c>1\),我们不妨先考虑颜色 \(1\) 的所有边。我们把所有点 \(u\) 按照以 \(u\) 结尾的最长 \(1\) 路径长度来划分等价类,那么一个等价类内部是不能有边的。现在只考虑颜色 \(1\) 带来的限制,对于每个等价类点数不超过 \(k^{c-1}\)
由于等价类最多有 \(k\) 个,那么可以得到 \(n=\sum|V_i|\leq k\cdot k^{c-1}=k^c\),但是其他颜色在等价类之间的边我们并没有考虑,这些边也有可能带来限制的。那我们考虑放宽限制,因为考虑了这些限制点数只会变少,所以 \(n\leq k^c\) 仍然成立。
构造方法就蕴含在证明中,我们把所有点均分成 \(k\) 个等价类递归下去,回溯上来时每个等价类都用 \(1\) 边相连。时间复杂度 \(O(n^2)\),这种构造方法能取到下界的原因是一直没有增添其他颜色边的限制。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,k,mx,a[M][M];
void solve(int l,int r,int c)
{
if(l==r) return ;
mx=max(mx,c);
int s=(r-l+k)/k;
for(int i=l;i<=r;i+=s)
{
int h=min(i+s-1,r);
solve(i,h,c+1);
for(int j=l;j<i;j++)
for(int k=i;k<=h;k++)
a[j][k]=c;
}
}
signed main()
{
n=read();k=read();
solve(1,n,1);
printf("%d\n",mx);
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
printf("%d ",a[i][j]);
}