大概就 lxr 讲了 4 个 AGC 的 D,有一个以前做过了不算,另外三个都会做罢(

为了避免开三个博客就把它们合并到一起了

AGC 037 D

lxr:难度顺序排列大概是 037<043<050

没错我也是这么认为的,如果按照我思考的时间来看,大概是 037 10min,043 50min,050 1h

首先注意到此题操作的特殊性:行能够交换两次,而列只能被交换一次,因此对于每个 \(a_{i,j}\),它必须在第二次(也就是 \(B\to C\) 这一步)从第 \(i\) 行交换到第 \(\lfloor\dfrac{a_{i,j}-1}{m}\rfloor\) 行,因此我们考虑对于这样的 \(a_{i,j}\) 连一条 \(i\to\lfloor\dfrac{a_{i,j}-1}{m}\rfloor\) 的边,那么对于 \(B\) 矩阵同一列上的元素对应的边,它们必须满足起点互不相同,终点也互不相同。看到这样的字眼对网络流熟悉一些的同学应该不难想到二分图匹配,具体来说我们将每个点裂成两个,对于每条原图中的边 \(u\to v\) 看作一条左边第 \(u\) 个点连向右边第 \(v\) 个点的边,那么 \(B\) 每列上的元素对应的边构成了一个匹配。这样一来就好办了,我们只要将原二分图的边集拆成 \(m\) 个匹配即可。我们考虑每次都找到一个匹配并将这些边从原图中删掉,可以证明每次删掉一个匹配后,都还能找到一个完美匹配。具体证明大概是由于原图是一个 \(n\) 阶正则图,可以考虑反证法,假设 \(\exists V\in\text{Left},s.t.|\text{Nb}(V)|<|V|\),那么 \(|\text{Nb}(V)|\) 与 \(|V|\) 之间边数应为 \(n|V|\),根据抽屉原理必然 \(\exists u\in\text{Nb}(V),s.t.\text{deg}(u)>n\),矛盾。这样也变相说明了 \(k\) 阶正则二分图必然存在完美匹配。

时间复杂度大概是 \(n^{3.5}\),如果 \(n,m\) 视作同阶。

const int MAXN=100;
const int MAXV=202;
const int MAXE=1e5;
const int INF=0x3f3f3f3f;
int n,m,a[MAXN+5][MAXN+5],b[MAXN+5][MAXN+5],c[MAXN+5][MAXN+5],used[MAXN*MAXN+5];
int S,T,hd[MAXV+5],to[MAXE+5],nxt[MAXE+5],cap[MAXE+5],ec=1,eid[MAXE+5];
void adde(int u,int v,int f,int id){
// printf("%d %d %d %d\n",u,v,f,id);
to[++ec]=v;cap[ec]=f;nxt[ec]=hd[u];hd[u]=ec;eid[ec]=id;
to[++ec]=u;cap[ec]=0;nxt[ec]=hd[v];hd[v]=ec;eid[ec]=id;
} int dep[MAXV+5],now[MAXV+5];
bool getdep(){
memset(dep,-1,sizeof(dep));dep[S]=0;
queue<int> q;q.push(S);now[S]=hd[S];
while(!q.empty()){
int x=q.front();q.pop();
for(int e=hd[x];e;e=nxt[e]){
int y=to[e],z=cap[e];
if(z&&!~dep[y]){
dep[y]=dep[x]+1;
now[y]=hd[y];q.push(y);
}
}
} return ~dep[T];
}
int getflow(int x,int f){
if(x==T) return f;int ret=0;
for(int &e=now[x];e;e=nxt[e]){
int y=to[e],z=cap[e];
if(z&&dep[y]==dep[x]+1){
int w=getflow(y,min(f-ret,z));
ret+=w;cap[e]-=w;cap[e^1]+=w;
if(f==ret) return ret;
}
} return ret;
}
int dinic(){
int ret=0;
while(getdep()) ret+=getflow(S,INF);
return ret;
}
int main(){
scanf("%d%d",&n,&m);T=(S=n<<1|1)+1;
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&a[i][j]);
for(int i=1;i<=m;i++){
memset(hd,0,sizeof(hd));ec=1;
for(int x=1;x<=n;x++) for(int y=1;y<=m;y++) if(!used[(x-1)*m+y]){
int should=(a[x][y]+m-1)/m;adde(x,should+n,1,(x-1)*m+y);
} for(int j=1;j<=n;j++) adde(S,j,1,0),adde(j+n,T,1,0);
assert(dinic()==n);
for(int j=1;j<=n;j++) for(int e=hd[j];e;e=nxt[e]){
if(to[e]!=S&&cap[e^1]){
int id=eid[e],x=(id+m-1)/m,y=(id-1)%m+1;
b[j][i]=c[to[e]-n][i]=a[x][y];used[id]=1;
// printf("used %d %d %d\n",id,i,e);
}
}
}
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) printf("%d%c",b[i][j]," \n"[j==m]);
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) printf("%d%c",c[i][j]," \n"[j==m]);
return 0;
}

AGC 043 D

一道挺有意思的数数题。

首先碰到这样的问题我们肯定要考虑什么样的排列符合要求。我们考虑 \(n\) 的位置——显然只有在迫不得已,即另外 \(n-1\) 个指针都已经被摧毁的时候才会将 \(n\) 加入序列,因此在 \(n\) 后面的元素肯定跟 \(n\) 是在一组的,我们完全可以把这些元素捆在一起,显然如果 \(n\) 后面元素个数 \(\ge 4\) 就直接不合法了,否则我们考虑删除这些元素,接着考虑序列中最大的元素,依次类推下去即可。因此对于一个排列 \(p\),它可以成为合法的排列的必要条件是:如果我们每次找出序列最大的元素并将其后面的元素全部删除,那么每次删除的元素个数 \(\le 3\)。为什么说是“必要条件”呢?因为考虑排列 \(p=(2,1,4,3,6,5)\),虽然它每次删除的数的个数都 \(\le 3\)(\(=2\)),但就是无法找到某个排列 \(p'\) 使得 \(p'\) 能够生成 \(p\)。因为我们不仅要将这些元素捆在一起,还要将它们组成若干个三元组,而三个 \(2\) 无法通过组合变成两个 \(3\),因此不可行。所以我们还要使拆出的数能够构成若干个三元组,\(3\) 自然可以单独成一组,我们 duck 不必管它,\(2\) 显然只能和 \(1\) 配对,如果 \(2\) 的个数严格多于 \(1\) 的个数那就会存在多余的 \(2\),也就不合法了,因此我们还要让拆出来的 \(2\) 的个数 \(\le 1\) 的个数。

到这里此题排列 \(p\) 的性质都已经分析完了,我们先考虑如何暴力地求答案,以 \(n=2\) 为例,我们枚举拆出的每段按顺序的值分别是多少,显然共有 \(1,1,1,1,1,1\)、\(1,1,1,1,2\)、\(1,1,1,2,1\)、\(1,1,2,1,1\)、\(1,2,1,1,1\)、\(2,1,1,1,1\)、\(1,1,1,3\)、\(1,1,3,1\)、\(1,3,1,1\)、\(3,1,1,1\),\(1,1,2,2\)、\(1,2,1,2\)、\(1,2,2,1\)、\(2,1,1,2\)、\(2,1,2,1\)、\(2,2,1,1\)、\(1,2,3\)、\(1,3,2\)、\(2,1,3\)、\(2,3,1\)、\(3,1,2\)、\(3,2,1\)、\(3,3\) 这些种可能,考虑怎样计算每一种可能的答案,以 \(1,3,2\) 为例,显然由于最后一组为 \(2\),因此 \(6\) 必须填第 \(5\) 个位置,这样一来最后一个元素就有 \(5\) 种可能,确定最后一个元素后,剩余 \(4\) 个元素中最大值的位置就定下来了,剩下来 \(a_3,a_4\) 还有 \(A_{3}^2=6\) 种可能,第一个元素也就只有 \(1\) 种选择了,所以这种情况的贡献为 \(30\),建议读者手算一下 \(n=2\) 的情况,看看是不是 \(261\),虽然有点烦但对正解有很强的启发性作用。

相信通过上面手算的过程我们可以看出,假设从左到右每一段长度分别为 \(a_1,a_2,\cdots,a_k\),那么方案数就是 \(\dfrac{n!}{a_1(a_1+a_2)(a_1+a_2+a_3)\cdots(a_1+a_2+\cdots+a_k)}\),这样就可以 \(dp\) 了,\(dp_{i,j}\) 表示考虑到第 \(i\) 个元素,\(1\) 的个数减去 \(2\) 的个数等于 \(j\) 的方案数,转移显然可以 \(\mathcal O(1)\),总复杂度 \(n^2\)。

const int MAXN=6000;
const int DELTA=6002;
int n,mod,ans=0;
int dp[MAXN+5][MAXN*2+5];
void add(int &x,int v){((x+=v)>=mod)&&(x-=mod);}
int main(){
scanf("%d%d",&n,&mod);n*=3;dp[0][DELTA]=1;
for(int i=0;i<n;i++) for(int j=DELTA-n;j<=DELTA+n;j++) if(dp[i][j]){
add(dp[i+1][j+1],dp[i][j]);
if(i+2<=n) add(dp[i+2][j-1],1ll*dp[i][j]*(i+1)%mod);
if(i+3<=n) add(dp[i+3][j],1ll*dp[i][j]*(i+1)%mod*(i+2)%mod);
} for(int i=DELTA;i<=DELTA+n;i++) add(ans,dp[n][i]);
printf("%d\n",ans);
return 0;
}

当然 lxr 总有比我更 nb 的做法,我们不考虑什么 DP,直接枚举 \(2,3\) 的个数计算方案数,假设 \(1,2,3\) 个数分别为 \(c_1,c_2,c_3\),那么显然对于每个块我们给它们填上数的方案数是一个多重组合数的形式,即 \(\dbinom{n}{1,1,\cdots,1,2,2,\cdots,2,3,3,\cdots,3}\),其中下面 \(1,2,3\) 分别是 \(c_1,c_2,c_3\) 个,套用多重组合数公式算一下是 \(\dfrac{n!}{2^{c_2}6^{c_3}}\),那么显然我们给这些块填好数后,我们肯定会按照最大数的大小从小到大将这些块排成一列,当然所有 \(1,2,3\) 都视作相同元素,因此还需除以 \(\dfrac{1}{c_1!c_2!c_3!}\)。对于长度为 \(3\) 的块还有 \(3,1,2\) 和 \(3,2,1\) 两种填数的方法,因此还要乘上 \(2^{c_3}\),故对于某对 \(c_1,c_2,c_3\),填数的方案数就是 \(\dfrac{n!}{c_1!c_2!c_3!2^{c_2}3^{c_3}}\),这个随便算算就行了。

不过似乎这个做法依赖于 \(m\) 是质数?

代码?sorry,暂(yong)时(jiu)没有代码,不过代码难度这么低的题为什么还要参考别人的代码呢?

AGC 050 D

一个垃圾的 \(\mathcal O(n^5)\) 的解法。

首先一件非常显然的事情是游戏最多进行 \(k\) 轮,因为 \(k\) 轮以后所有物品的位置都已经知道了。而且对于一个人 \(i\),如果它到了第 \(j\) 轮还没有结束游戏,那么必然前 \(j-1\) 轮随机点到的物品都被抢过了,因此如果假设第 \(j\) 轮已经有 \(l\) 个物品被抢过了,那么第 \(j\) 轮 \(i\) 抢到物品的概率就是 \(\dfrac{k-l}{k-j+1}\)。

有了这个性质之后考虑怎样计算答案。首先我们肯定要对所有人计算答案对吧,我们假设现在要对 \(i\) 计算答案,由于抢物品有先后顺序,我们仅仅记录每一轮有多少个物品被抢走,也即有多少个人抢到了物品是不够的,不过注意到在 \(i\) 前面和在 \(i\) 后面的人实际上都是等价的,因此我们只需记录 \(i\) 前面和 \(i\) 后面分别由多少个人拿到了物品即可确定每一步拿到物品的概率。因此考虑设 \(dp_{j,l,x,y}\) 表示当前考虑到了第 \(j\) 轮,现在第 \(j\) 轮中前 \(l\) 个人已经随机选择了物品,\(i\) 前面和后面分别有 \(x\) 个人和 \(y\) 个人拿到了物品的概率,转移就 xjb 分类讨论即可。

时间复杂度 \(\mathcal O(n^5)\)

并不知道你们所说的 \(\mathcal O(n^4)\) 的解法是啥

const int MAXN=40;
const int MOD=998244353;
int n,k,inv[MAXN+5];
int dp[MAXN+5][MAXN+5][MAXN+5][MAXN+5];
int main(){
scanf("%d%d",&n,&k);
for(int i=(inv[0]=inv[1]=1)+1;i<=MAXN;i++) inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=1;i<=n;i++){
memset(dp,0,sizeof(dp));dp[0][n][0][0]=1;int res=0;
for(int j=1;j<=k;j++){
for(int x=0;x<=i-1;x++) for(int y=0;y<=n-i;y++)
dp[j][0][x][y]=dp[j-1][n][x][y];
for(int l=1;l<=n;l++){
for(int x=0;x<=i-1;x++) for(int y=0;y<=n-i;y++){
int p=1ll*(k-x-y)*inv[k-j+1]%MOD;
// printf("%d %d %d %d %d\n",j,l-1,x,y,dp[j][l-1][x][y]);
if(l==i){
dp[j][l][x][y]=(dp[j][l][x][y]+1ll*dp[j][l-1][x][y]*(1-p+MOD))%MOD;
res=(res+1ll*dp[j][l-1][x][y]*p)%MOD;
} else if(l<i){
if(l<=x) dp[j][l][x][y]=(dp[j][l][x][y]+dp[j][l-1][x][y])%MOD;
else{
dp[j][l][x+1][y]=(dp[j][l][x+1][y]+1ll*dp[j][l-1][x][y]*p)%MOD;
dp[j][l][x][y]=(dp[j][l][x][y]+1ll*dp[j][l-1][x][y]*(1-p+MOD))%MOD;
}
} else {
if(l-i<=y) dp[j][l][x][y]=(dp[j][l][x][y]+dp[j][l-1][x][y])%MOD;
else{
dp[j][l][x][y+1]=(dp[j][l][x][y+1]+1ll*dp[j][l-1][x][y]*p)%MOD;
dp[j][l][x][y]=(dp[j][l][x][y]+1ll*dp[j][l-1][x][y]*(1-p+MOD))%MOD;
}
}
}
}
} printf("%d\n",res);
}
return 0;
}

最新文章

  1. Kotlin中变量不同于Java: var 对val(KAD 02)
  2. .net ServiceStack.Redis 性能调优
  3. C# 大小写转换,方便index of
  4. iOS8后core location框架启动定位服务的步骤
  5. Linux shell文本过滤
  6. javascript中字符串的常用方法
  7. js判断密码强度
  8. linux 常用alias
  9. IT安全的本质
  10. ha_innobase::open
  11. 在xcode上搭建OpenGL3.x运行环境
  12. Django学习笔记(一)
  13. eclipse中创建NDK和JNI开发环境最简单配置方法
  14. c++文件读取(一)---输入类使用和查找当前程序路径
  15. source is null for getProperty(null, &quot;cpmodel&quot;)异常结局
  16. Dynamics CRM FORM脚本库加载本地脚本
  17. [原创]K8Cscan插件之端口扫描C#源码
  18. 一个真实的Async/Await示例
  19. mysql 案例 ~ 常见案例汇总
  20. ECUST Div2 训练赛3 (只有代码)

热门文章

  1. 小白自制Linux开发板 六. SPI TFT屏幕修改与移植
  2. 【UE4 C++ 基础知识】&lt;11&gt;资源的同步加载与异步加载
  3. 【二食堂】Alpha - Scrum Meeting 8
  4. BUAA_2020_软件工程_结对项目作业
  5. 基于openeuler的openssl编程
  6. linux 蓝牙开发调试(rtl8821cs模块)
  7. 第36篇-return字节码指令
  8. python解释器的下载与安装
  9. 【前端工具】nodejs+npm+vue 安装(windows)
  10. Linux下软链接与硬链接的区别