2020ICPC江西省大学生程序设计竞赛
比赛链接
2020ICPC 江西省大学生程序设计竞赛
A.Simple Math Problem
求解 \(\sum_{i=1}^{n} \sum_{j=1}^{i}[\operatorname{gcd}(i, j)==1] f(j)\),其中 \(f(j)\) 为 \(j\) 的数位和
解题思路
暴力,容斥原理
先预处理所有的 \(f(j)\),对于每个 \(f(j)\),找出 \(j\leq i\leq n\) 内 \(i\) 与 \(j\) 互质的个数 \(cnt\),\(cnt\times f(j)\) 即为 \(j\) 的贡献,关键在于计算 \(cnt\),根据前缀和,即找 \(1\leq i\leq n\) 内 \(i\) 与 \(j\) 互质的个数,由于 \(n\geq j\),直接计算与 \(j\) 互质的个数不好做,相反计算与 \(j\) 不互质的个数比较方便,即先对 \(j\) 分解质因数种类:\(p_1,p_2,\dots,p_k\),然后找 \(1\sim n\) 内倍数为 \(p_1,\dots,p_k\) 的数个数,其中也得用到容斥原理
设平均质因子种数为 \(C\),则:
- 时间复杂度:\((n\times C\times 2^C)\)
莫比乌斯函数
\(\sum_{i=1}^{n} \sum_{j=1}^{i} f(j)[(i, j)=1]\) 等价于 \(\sum_{i=1}^{n} \sum_{j=i}^{n} f(i)[(i, j)=1]\),由容斥原理,\(\sum_{i=1}^{n} \sum_{j=i}^{n} f(i)[(i, j)=1]\Leftrightarrow\)\(\sum_{i=1}^{n} \sum_{j=1}^{n} f(i)[(i, j)=1]-\sum_{i=1}^{n} \sum_{j=1}^{i} f(i)[(i, j)=1]+f(1)\),而 \([(i, j)=1]=\sum_{d\mid (i,j)}\mu(d)\),则 \(\Leftrightarrow \sum_{i=1}^{n} \sum_{j=1}^{n} f(i)\sum_{d\mid (i,j)}\mu(d)-\sum_{i=1}^{n} f(i) \phi(i)+f(1)\),将 \(d\) 提到外面,\(d\) 的范围为 \([1,n]\),此时需满足 \(d\mid (i,j)\),设 \(i'=i/d,j'=j/d\),变换,\(\Leftrightarrow \sum_{d=1}^n\mu(d)\frac{n}{d} \sum_{i'=1}^{\frac{n}{d}}f(i'd)-\sum_{i=1}^{n} f(i) \phi(i)+f(1)\)
求解后面一部分,直接线性筛求解欧拉函数即可,前面一部分计算 $ \sum_{i'=1}^{\frac{n}{d}}f(i'd)$ 需 \(O(调和级数)\) 复杂度求解,故:
- 时间复杂度:\(O(nlogn)\)
代码
- 莫比乌斯函数
// Problem: A Simple Math Problem
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/A
// Memory Limit: 262144 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=1e5+5;
int n,prime[N],m,v[N],phi[N],u[N],f[N],g[N];
void init(int n)
{
u[1]=phi[1]=f[1]=1;
for(int i=2;i<=n;i++)
{
f[i]=f[i/10]+i%10;
if(v[i]==0)
{
prime[++m]=v[i]=i;
u[i]=-1;
phi[i]=i-1;
}
for(int j=1;j<=m&&i*prime[j]<=n;j++)
{
if(v[i]<prime[j])break;
if(i%prime[j]==0)u[i*prime[j]]=0;
else
u[i*prime[j]]=-u[i];
v[i*prime[j]]=prime[j];
phi[i*prime[j]]=phi[i]*(i%prime[j]?prime[j]-1:prime[j]);
}
}
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j+=i)g[i]+=f[j];
}
int main()
{
cin>>n;
init(n);
LL res=0;
for(int i=1;i<=n;i++)
res+=1ll*u[i]*(n/i)*g[i]-f[i]*phi[i];
res+=f[1];
cout<<res;
return 0;
}
- 暴力
// Problem: A Simple Math Problem
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/A
// Memory Limit: 262144 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=1e5+5;
int f(int x)
{
int res=0;
while(x)res+=x%10,x/=10;
return res;
}
int F[N],n,prime[N],m,v[N];
void init(int n)
{
for(int i=2;i<=n;i++)
{
if(v[i]==0)
v[i]=prime[++m]=i;
for(int j=1;j<=m&&i*prime[j]<=n;j++)
{
if(v[i]<prime[j])break;
v[i*prime[j]]=prime[j];
}
}
for(int i=1;i<=n;i++)F[i]=f(i);
}
vector<int> Div(int x)
{
vector<int> res;
unordered_set<int> S;
while(v[x])
{
if(!S.count(v[x]))S.insert(v[x]),res.pb(v[x]);
x/=v[x];
}
return res;
}
int cal(int x,int n)
{
vector<int> a=Div(x);
int res=0;
if(x>n)
{
res=x;
for(int i:a)res/=i,res*=(i-1);
if(x==1)res--;
return res;
}
res=n;
int sz=a.size();
for(int i=0;i<(1<<sz);i++)
{
int t=1,sign=1;
for(int j=0;j<sz;j++)
if(i>>j&1)t*=a[j],sign*=-1;
if(t!=1)//注意
res+=sign*n/t;
}
return res;
}
int main()
{
init(N-1);
cin>>n;
LL res=0;
for(int i=1;i<=n;i++)res+=1ll*F[i]*(cal(i,n)-cal(i,i-1));
cout<<res;
return 0;
}
C.Charging
数轴上有 \([1, n]\) 一共 \(n\) 个点, \(m\) 个区间分别是 \(\left[l_{i}, r_{i}\right]\) ,设 \(tot\) 为所选取的区间数量, \(x\) 为所有所选取的区间的交集长度, 求 \(\min (\) tot,\(x)\) 的最大值
解题思路
树状数组,二分
所选取的交集区间一定是某个区间的左端点,某个区间的右端点,不妨枚举所选取区间的右端点 \(r\),即按将所有区间按右端点从大到小排序,枚举右端点时前面的点的右端点都满足右端点条件,设所选取区间的左端点为 \(l\),则利用树状数组可计算前面的区间左端点不大于 \(l\) 的点即满足包含区间 \([l,r]\) 的区间的个数,即选取的区间的个数 \(f=(l)=tot\),此时交集长度为 \(r-l+1\),而要求 \(min(r-l+1,f(l))\) 最大,其中 \(r\) 固定,\(r-l+1\) 为减函数,\(f(l)\) 为增函数,当两者最接近时最优,故可二分 \(l\),使其满足 \(r-l+1\leq f(l)\) 的最小的 \(l\),答案在 \(l\) 或 \(l-1\) 中
- 时间复杂度:\(O(nlognlogn)\)
代码
// Problem: Charging
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/C
// Memory Limit: 262144 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
// #define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=3e5+5;
int n,m,res,tr[N];
struct A
{
int l,r;
bool operator<(const A &o)
{
if(r==o.r)return l<o.l;
return r>o.r;
}
}a[N];
int ask(int x)
{
int res=0;
for(;x;x-=x&-x)res+=tr[x];
return res;
}
void add(int x,int y)
{
for(;x<N;x+=x&-x)tr[x]+=y;
}
int main()
{
read(n),read(m);
for(int i=1;i<=m;i++)read(a[i].l),read(a[i].r);
sort(a+1,a+1+m);
for(int i=1;i<=m;i++)
{
int l=1,r=a[i].r;
add(a[i].l,1);
while(l<r)
{
int mid=l+r>>1;
if(a[i].r-mid+1<=ask(mid))r=mid;
else
l=mid+1;
}
res=max(res,min(ask(l),a[i].r-l+1));
l--;
res=max(res,min(ask(l),a[i].r-l+1));
}
cout<<res;
return 0;
}
D.Chinese Valentine's Day
给出 \(n\) 个整数字符串,要求所有本质不同的子串的和
解题思路
后缀自动机
本质上是广义后缀自动机,但可转化为普通后缀自动机,即在 \(n\) 个字符串之间插入一个分割字符,将 \(n\) 个字符串合并为一个字符串,构建 \(SAM\),由于 \(SAM\) 的有向无环图中的所有路径与所有子串一一对应,设某一个状态节点 \(u\) 的贡献为 \(res[u]\),每次按某一个字符 \(i\) 扩展到状态节点 \(v\),贡献转移即 \(res[v]+=10\times res[u]+i\times sz[u]\),其中 \(sz[i]\) 表示状态节点 \(i\) 表示的子串数量即 \(|endpos[i]|\),每次扩展需要前一个状态转移过来,拓扑排序计算贡献即可,同时计算贡献时只需在非分割字符扩展时计算
- 时间复杂度:\(O(\sum_{i=1}^{n}|s[i]|)\)
代码
// Problem: Chinese Valentine's Day
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/D
// Memory Limit: 524288 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int mod=1e9+7,N=3e6+5;
int n,lst=1,cnt=1,sz[N],res[N],in[N];
char s[N];
struct Node
{
int len,fa;
int ch[11];
}node[N];
void extend(int c)
{
int p=lst,np=lst=++cnt;
node[np].len=node[p].len+1;
for(;p&&!node[p].ch[c];p=node[p].fa)node[p].ch[c]=np;
if(!p)node[np].fa=1;
else
{
int q=node[p].ch[c];
if(node[q].len==node[p].len+1)node[np].fa=q;
else
{
int nq=++cnt;
node[nq]=node[q];
node[nq].len=node[p].len+1;
node[q].fa=node[np].fa=nq;
for(;p&&node[p].ch[c]==q;p=node[p].fa)node[p].ch[c]=nq;
}
}
}
void topSort()
{
queue<int> q;
for(int i=1;i<=cnt;i++)
{
for(int j=0;j<=10;j++)
{
int to=node[i].ch[j];
if(to)in[to]++;
}
}
for(int i=1;i<=cnt;i++)
if(!in[i])
{
sz[i]=1;
q.push(i);
}
while(q.size())
{
int x=q.front();
q.pop();
for(int i=0;i<=10;i++)
{
int y=node[x].ch[i];
if(!y)continue;
if(i!=10)
{
res[y]=(res[y]+1ll*res[x]*10+i*sz[x])%mod;
sz[y]=(sz[y]+sz[x])%mod;
}
if(--in[y]==0)q.push(y);
}
}
}
int main()
{
scanf("%d",&n);
while(n--)
{
scanf("%s",s);
for(int i=0;s[i];i++)extend(s[i]-'0'); ;
extend(10);
}
topSort();
LL ret=0;
for(int i=2;i<=cnt;i++)
ret=(ret+res[i])%mod;
cout<<ret;
return 0;
}
E.Color Sequence
给定一个颜色序列,求它有多少个颜色出现次数都是偶数的连续子序列
解题思路
思维
颜色数量比较少,可以考虑用二进制来表示每一种颜色,要求每种颜色出现次数为偶数,即表示的连续子序列异或值为 \(0\),不妨先求出前缀异或值 \(s\),考虑每个数的贡献,对于当前数下标 \(i\),求前面有多少个数 \(j\),满足 \(s[i]\oplus s[j-1]=0\),直接统计前面 \(s[i]\) 的个数即可
- 时间复杂度:\(O(n)\)
代码
// Problem: Color Sequence
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/E
// Memory Limit: 131072 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
// #define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=1e6+5;
int n,s[N],cnt[1<<21];
LL res;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
s[i]=s[i-1]^(1<<x);
}
cnt[0]=1;
for(int i=1;i<=n;i++)res+=cnt[s[i]],cnt[s[i]]++;
cout<<res;
return 0;
}
F.Magical Number
定义魔数:其前缀表示的数都能整除该数的位数,每一个个位数都由下列火柴棒组成:
求 \(n\)(\(1≤n≤10^{100}\)) 个火柴棒恰能组成的最大的数
解题思路
暴力dfs
对于 \(i\) 位的魔数 \(x\),后面组成的数 \(y=x+[0,9]\),共有 \(10\) 条分支,要求 \(y\) 整除 \(i+1\),这样的条件可想而知,分支数会越来越少,不妨考虑暴力 \(dfs\),用 \(int\) 速度控制在了 \(1\) 秒内,不妨直接用 \(\_\_int128\),会发现最大值为 \(3608528850368400786036725\),没有超出 \(\_\_int128\) 的范围,所以位数大概到了 \(24\) 的量级就没有出现分支了
共有 \(24\) 层,每层直接最多有 \(10\) 个分支,但实际上很少,故:
- 时间复杂度:\(O(常数)\)
代码
// Problem: Magical Number
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/F
// Memory Limit: 262144 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
template <typename T>
void print(T x) {
if (x < 0) putchar('-'), x = -x;
if (x < 10) putchar(x + 48);
else print(x / 10), putchar(x % 10 + 48);
}
#define int __int128
string s;
int n,res=-1,a[]={6,2,5,5,4,5,6,3,7,6};
void dfs(int len,int x,int cost)
{
if(cost<=0)
{
if(cost==0)res=max(res,x);
return ;
}
for(int i=0;i<=9;i++)
{
int y=x*10+i;
if(y%(len+1)==0)dfs(len+1,y,cost-a[i]);
}
}
signed main()
{
cin>>s;
if(s.size()<=3)
{
n=atoi(s.c_str());
for(int i=1;i<=9;i++)
dfs(1,i,n-a[i]);
}
print(res);
return 0;
}
G.Mathematical Practice
\(n\) 个数,要求 \(m\) 个子集,使得子集之间没有交集,求方案数
解题思路
放球问题
由于 \(n,m\) 比较大,考虑结论
问题转化:将 \(n\) 个不同的球放入 \(m\) 个不同盒子(不一定非要放入盒子中)的方案数,即经典放球问题,每个球有 \(m+1\) 种选择,答案即为 \((m+1)^n\)
- 时间复杂度:\(O(logn)\)
代码
// Problem: Mathematical Practice
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/G
// Memory Limit: 262144 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int mod=998244353;
int n,m;
int ksm(int a,int b,int p)
{
int res=1%p;
while(b)
{
if(b&1)res=1ll*res*a%p;
a=1ll*a*a%p;
b>>=1;
}
return res;
}
int main()
{
cin>>n>>m;
cout<<ksm(m+1,n,mod);
return 0;
}
J.Split Game
一张 \(n \times m\) 的纸,两个人轮流操作,可以沿着一条直线切,当有人切出 \(1 \times 1\) 的纸时输掉比赛,问最后谁赢
解题思路
博弈论,sg函数
跟 219. 剪纸游戏 不同的是,这里要求切出 \(1 \times 1\) 的纸时输掉比赛,则 \(1\times 1\) 为终止态,在进行剪纸时不能剪到 \(1\times 1\) 的状态再计算 mex 即可
- 时间复杂度:\(O(n^3)\)
代码
// Problem: Split Game
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/J
// Memory Limit: 262144 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=155;
int f[N][N],n,m;
int sg(int n,int m)
{
if(f[n][m]!=-1)return f[n][m];
unordered_set<int> s;
for(int i=1;i<=n-i;i++)
if(m!=1||i!=1&&n-i!=1)s.insert(sg(i,m)^sg(n-i,m));
for(int i=1;i<=m-i;i++)
if(n!=1||i!=1&&m-i!=1)s.insert(sg(n,i)^sg(n,m-i));
for(int i=0;;i++)
if(!s.count(i))return f[n][m]=f[m][n]=i;
}
int main()
{
memset(f,-1,sizeof f);
f[1][1]=0;
while(cin>>n>>m)puts(sg(n,m)?"Alice":"Bob");
return 0;
}
L.WZB's Harem
给你一张 \(n \times n(n \leq 20)\) 的图,图上有 \(0\) 和 \(1\) 两种数字,现在有 \(n\) 个互不相同的皇后,需要将这 \(n\) 个皇后放在 \(0\) 的位置上,并且保证这 \(n\) 个皇后之间不存在任意两个皇后处于同一行、同一列,问方案数。
解题思路
状压dp
- 状态表示:\(f[i][j]\) 表示前 \(i\) 行,状态为 \(j\) 的方案数,\(j\) 表示前 \(i\) 个皇后选在哪些列的状态
- 状态计算:\(f[i][j|1<<k]+=f[i-1][j]\)
分析:\(j\) 二进制下含有 \(i-1\) 个 \(1\),且第 \(k\) 位为 \(0\),则在 \(a[i][j]=0\) 的前提下可转移到第 \(i\) 行
\(O(n^2)\) 枚举行和列,再枚举含有对应 \(1\) 的状态,最坏情况下有 \(C_n^{\frac{n}{2}}\) 种状态,则:
- 时间复杂度:\(O(n^2\times C_n^{\frac{n}{2}})\)
代码
// Problem: WZB's Harem
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/36726/L
// Memory Limit: 262144 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=20,mod=1e9+7;
int f[N][1<<N],n,a[N][N];
vector<int> b[N];
int main()
{
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)cin>>a[i][j];
for(int i=0;i<(1<<n);i++)b[__builtin_popcount(i)].pb(i);
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
{
if(a[i][j])continue;
for(int k=0;k<b[i].size();k++)
{
if((b[i][k]>>j&1)==0)
{
if(i)
f[i][b[i][k]|1<<j]=(f[i][b[i][k]|1<<j]+f[i-1][b[i][k]])%mod;
else
f[i][b[i][k]|1<<j]++;
}
}
}
int res=f[n-1][(1<<n)-1];
for(int i=1;i<=n;i++)res=1ll*res*i%mod;
cout<<res;
return 0;
}