七夕祭
给出一个\(n\times m\)的网格图,以及t个棋子的位置,每次操作可以选择将一个棋子向上下左右移动,注意第一行与第n行相邻,第一列与第n列相邻,如棋子可以从第一行移动到第n行,现在有2个条件
- 保证每一行的棋子数相同
- 保证每一列的棋子数相同
询问尽量满足上诉要求的最少移动次数,\(1≤n,m≤100000\)。
解
注意到一个性质,当棋子上下移动的时候,只会改变每一行拥有的棋子数,而棋子左右移动只会改变每一列拥有的棋子数,换个意思就是棋子的上下移动和左右移动可以作为两个问题看待,也就是要求可以分别处理,这样问题就大大简化了,接下来设棋子数为\(tot\)。
于是设\(h[i]\)表示第i行拥有的棋子数,\(l[i]\)为第i列拥有的棋子数,不妨拿行出来研究,现在的问题转化成,数列\(\{h[i]\}\)的中间一个位置可将自己的部分数字交给它相邻的位置,而这个数字就是操作次数,每一行要平均分到\(tot/n\),如果\(tot\% n\)不为0显然不能均分,显然这个问题类似均分纸牌,如果设\(\{H_i\}\)为\(\{h_i\}\)的前缀和,那么对于数列前i个数来看,它拥有的数有\(H_i\),但是它只需要\(tot/n\times i\),于是第i+1个数要交给或接受这一个整体\(|H_i-tot/n\times i|\),于是容易知道当不存在第一行与最后一行相邻的时候,我们有答案\(\sum_{i=1}|H_i-tot/n\times i|\)。
但是注意相邻,类似一个环,于是我们可以考虑处理环的基本办法,这道题采取了二次递推,名不符实,因为二次递推是递推里的做法,但基本思想都是拆环以后,根据一个拆环的维护的数据来推出其他拆环的数据(犹如二次扫描+换根法)
此时按照均分纸牌的思路容易知道,答案应该为
很难想到设\(f_i=H_i-tot/n\times i\)的前缀和,于是有
因为\(f_i\)的绝对值表示前i个位置需要第i+1个位置所或者给的数字,于是\(f_n=0\),因为前n个位置不要得到数字,否则无解(从代数上来看\(f_n=H_n-tot/n\times n=H_n-tot=tot-tot=0\)),所以有
现在问题就转化成了求出哪个\(f_k\)可以使这个式子最小化,显然这就是列车调度,取中位数即可,时间复杂度可以做到\(O((n+m)log(n))\)。
我已经很久不小结题目了,但是此题实在是让我认识到\({\large \text{我太菜了}}\)。
先总结一下网格图的基本做法
首先它的考虑点应该是从行列,对角线,矩形,轮廓来考虑的(细节是\(1\times n,n\times 1\)的网格图,你该输出什么)
而一些比较骚的做法是拆行成列(八数码问题,高斯消元)和行列独立(看看行列之间的关系是否独立,例子比如此题还有一大堆错排问题)
然后就是环的问题处理,首要拆环成链,然后考虑是要再补一截,记录一截还是二次递推
而接下来的代数变换就骚的教我做人,根本想不到设哪个为一个整体,然后就推,发现原来是一个列车调度问题。
于是根据此题,我们得出一个著名的结论,\({\large \text{我太菜了}}\)。
参考代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#define il inline
#define ri register
#define ll long long
using namespace std;
ll ans;
int g[100001],k[100001],tot,check;
il void read(int&),work(int,int[]);
template<class free>
il free Abs(free);
int main(){int n,m,t;
read(n),read(m),read(t);
for(int i(1),y,x;i<=t;++i)
read(y),read(x),++g[y],++k[x],++tot;
if(!(tot%n))check|=1,work(n,g);if(!(tot%m))check|=2,work(m,k);
switch(check){
case 0:cout<<"impossible ";break;
case 1:cout<<"row ";break;
case 2:cout<<"column ";break;
case 3:cout<<"both ";break;
}if(check)printf("%lld",ans);
return 0;
}
template<class free>
il free Abs(free x){
return x<0?-x:x;
}
il void work(int n,int a[]){
for(int i(1);i<=n;++i)
a[i]-=tot/n,a[i]+=a[i-1];
sort(a+1,a+n+1);int mid(a[1+n>>1]);
for(int i(1);i<=n;++i)
ans+=Abs(mid-a[i]);
}
il void read(int &x){
x^=x;ri char c;while(c=getchar(),c<'0'||c>'9');
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
}