NewOJ周赛于2022年3月19日正式开始,比赛时间为每周六晚19:00-22:00。
比赛链接:http://oj.ecustacm.cn/contest.php?cid=1019
A 并行处理
题意: 现在有 n n n个任务需要到GPU上运行,但是只有两张GPU,每张GPU一次只能运行一个任务,两张GPU可以并行处理。告诉你 n n n个任务需要的时间,你需要选择一个数字 i i i:将任务 1 1 1到任务 i i i放到第一个GPU上运行,任务 i + 1 i+1 i+1到任务 n n n放到第二个GPU上运行。求最短运行时间。
Tag: 思维题,前缀和
难度: ☆
来源: C o d e C h e f E a s y CodeChef\ Easy CodeChef Easy
思路: 最短运行时间为 m i n { m a x ( ∑ j = 1 i a j , ∑ j = i + 1 n a j ) , i = 1 , . . . , n } min\{max(\sum_{j=1}^ia_j,\sum_{j=i+1}^na_j),i=1,...,n\} min{max(∑j=1iaj,∑j=i+1naj),i=1,...,n}。如果暴力计算每一个 i i i,则总的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
优化: 利用前缀和来优化里面那一项,最短运行时间为 m i n { m a x ( s u m [ i ] , s u m [ n ] − s u m [ i ] ) , i = 1 , . . . , n } min\{max(sum[i],sum[n]-sum[i]),i=1,...,n\} min{max(sum[i],sum[n]−sum[i]),i=1,...,n},这样时间复杂度为 O ( n ) O(n) O(n)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
ll sum[maxn];
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++)
{
int x;
cin >> x;
sum[i] = sum[i - 1] + x;
}
ll ans = sum[n];
for(int i = 1; i <= n; i++)
ans = min(ans, max(sum[i], sum[n] - sum[i]));
cout<<ans<<endl;
return 0;
}
B 排列变换
题意: 给定一个长度为 n n n的排列 a a a,需要将这个排列变成 b b b。每次可以选择一个数字往左移若干个位置。请求出最小移动次数。
Tag: 思维题,类逆序对
难度: ☆☆
来源: U S A C O 2022 F e b USACO\ 2022\ Feb USACO 2022 Feb
思路: 将排列 a a a转换成排列 b b b,首先需要处理出数组 c c c, c [ i ] c[i] c[i]表示数字 a [ i ] a[i] a[i]在 b b b中的下标。
例如样例 a = [ 5 , 1 , 3 , 2 , 4 ] , b = [ 4 , 5 , 2 , 1 , 3 ] a=[5,1,3,2,4],b=[4,5,2,1,3] a=[5,1,3,2,4],b=[4,5,2,1,3],对应的 c = [ 2 , 4 , 5 , 3 , 1 ] c=[2,4,5,3,1] c=[2,4,5,3,1]。 c c c表示当前排列 a a a对应的 b b b中的下标,则最终肯定要把 c c c变成 [ 1 , 2 , 3 , 4 , 5 ] [1,2,3,4,5] [1,2,3,4,5]。
则问题变成:每次可以将 c c c数组中的数字往左移,最终变成 [ 1 , 2 , 3 , 4 , 5 ] [1,2,3,4,5] [1,2,3,4,5],求最小移动次数。
什么时候一定要往左移?存在逆序的时候,即 c [ i ] c[i] c[i]的左边存在一个大于 c [ i ] c[i] c[i]的数字,则 c [ i ] c[i] c[i]一定要左移。
对于每个 c [ i ] c[i] c[i]判断左边是否有数字大于 c [ i ] c[i] c[i],暴力求解时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
优化: 维护一个从左往右的最大值,判断最大值是否大于 c [ i ] c[i] c[i]即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int a[maxn], b[maxn], c[maxn];
int id[maxn];///id[x]表示数字x在b数组中的下标
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++)
cin >> a[i];
for(int i = 1; i <= n; i++)
cin >> b[i], id[b[i]] = i;
///把a数组全部变成对应b数组的下标
for(int i = 1; i <= n; i++)
c[i] = id[a[i]];
int ans = 0, Max = 0;
for(int i = 1; i <= n; i++)
{
///只要目前c[i]左边有一个比c[i]大的数字,则答案++
if(Max > c[i])ans++;
Max = max(Max, c[i]);
}
cout<<ans<<endl;
return 0;
}
C 硬币游戏
题意: N N N个硬币,每次可以移除1个或者 p x p^x px个,其中 p p p为质数, x x x为正整数。均采取最优策略,求先手胜还是后手胜。
Tag: 博弈论,找规律
难度: ☆☆
来源: C o d e C h e f E a s y CodeChef\ Easy CodeChef Easy
思路: 小范围模拟,可以发现 N = 1 , 2 , 3 , 4 , 5 N=1,2,3,4,5 N=1,2,3,4,5时,均为必胜态,因为 2 , 3 , 5 2,3,5 2,3,5为质数, 4 = 2 2 4=2^2 4=22,而 N = 6 N=6 N=6为必败态。按照这个规律可以发现问题变成巴什博奕。
博弈基础:必胜态可以转换成必败态,而必败态只能转换成必胜态。
N N N只要是 6 6 6的倍数,那么一定是必败态。
1、如果 N % 6 = 0 N\%6=0 N%6=0,由于取出的数字要么是 1 1 1,要么是质数的幂,不可能取出一个 6 6 6的倍数,因此只能转移到非 6 6 6的倍数。
2、如果 N % 6 ≠ 0 N\%6\ne0 N%6=0,那么一定可以让对方面临 6 6 6的倍数。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int T;
cin >> T;
while(T--)
{
int n;
cin >> n;
if(n % 6 != 0)cout<<"Alice"<<endl;
else cout<<"Bob"<<endl;
}
return 0;
}
D 矩阵
题意: 给定两个 n ∗ n n*n n∗n的矩阵 A A A和 B B B,记 C = A ∗ B C=A*B C=A∗B(此处为矩阵乘法), m m m次询问,每次询问 C C C中一个子矩阵中所有数字之和。
Tag: 数学,前缀和,枚举
难度: ☆☆☆
来源: B Z O J 2901 BZOJ\ 2901 BZOJ 2901
思路: 矩阵乘法:
C
i
,
j
=
∑
k
=
1
n
A
i
,
k
B
k
,
j
C_{i,j}=\sum_{k=1}^nA_{i,k}B_{k,j}
Ci,j=∑k=1nAi,kBk,j,对于每次询问
a
,
b
,
c
,
d
a,b,c,d
a,b,c,d,询问子矩阵和为:
a
n
s
=
∑
i
=
a
c
∑
j
=
b
d
C
i
,
j
=
∑
i
=
a
c
∑
j
=
b
d
∑
k
=
1
n
A
i
,
k
B
k
,
j
=
∑
k
=
1
n
∑
i
=
a
c
∑
j
=
b
d
A
i
,
k
B
k
,
j
=
∑
k
=
1
n
[
(
∑
i
=
a
c
A
i
,
k
)
(
∑
j
=
b
d
B
k
,
j
)
]
\begin{aligned} ans &= \sum_{i=a}^c\sum_{j=b}^dC_{i,j} \\&=\sum_{i=a}^c\sum_{j=b}^d\sum_{k=1}^nA_{i,k}B_{k,j} \\&=\sum_{k=1}^n\sum_{i=a}^c\sum_{j=b}^dA_{i,k}B_{k,j} \\&=\sum_{k=1}^n\left[ \left(\sum_{i=a}^cA_{i,k}\right) \left(\sum_{j=b}^dB_{k,j}\right) \right] \end{aligned}
ans=i=a∑cj=b∑dCi,j=i=a∑cj=b∑dk=1∑nAi,kBk,j=k=1∑ni=a∑cj=b∑dAi,kBk,j=k=1∑n⎣⎡(i=a∑cAi,k)⎝⎛j=b∑dBk,j⎠⎞⎦⎤
可以发现,括号中的两个值分别可以用
A
A
A的列前缀和、
B
B
B的行前缀和
O
(
1
)
O(1)
O(1)获取。
∑
i
=
a
c
A
i
,
k
=
(
S
u
m
A
[
c
]
[
k
]
−
S
u
m
A
[
a
−
1
]
[
k
]
)
∑
i
=
b
d
B
k
,
j
=
(
S
u
m
B
[
k
]
[
d
]
−
S
u
m
B
[
k
]
[
b
−
1
]
)
\sum_{i=a}^cA_{i,k}=(Sum_A[c][k]-Sum_A[a-1][k]) \\ \sum_{i=b}^dB_{k,j}=(Sum_B[k][d]-Sum_B[k][b-1])
i=a∑cAi,k=(SumA[c][k]−SumA[a−1][k])i=b∑dBk,j=(SumB[k][d]−SumB[k][b−1])
因此,对于每次询问,暴力枚举
k
k
k即可,时间复杂度
O
(
n
2
+
m
∗
n
)
O(n^2+m*n)
O(n2+m∗n)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 1e9 + 7;
template <typename T>
inline T read(T& x) {
x = 0;
T w = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x = x * w;
}
const int maxn = 2010;
int A[maxn][maxn], B[maxn][maxn];
int main()
{
int n, m;
read(n); read(m);
///读入矩阵,同时处理前缀和
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
read(A[i][j]), A[i][j] += A[i - 1][j];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
read(B[i][j]), B[i][j] += B[i][j - 1];
while(m--)
{
int a, b, c, d;
read(a);read(b);read(c);read(d);
///可能存在a>c、b>d的情况
if(a > c)swap(a, c);
if(b > d)swap(b, d);
ll ans = 0;
for(int k = 1; k <= n; k++)
ans += (ll)(A[c][k] - A[a - 1][k]) * (B[k][d] - B[k][b - 1]);
printf("%lld\n", ans);
}
return 0;
}
E 可达点
题意: 在 n n n个点 m m m条边的有向图中,请你求出有多少个点是可达点。点 u u u为可达点:所有其他的点均可到达点 u u u。
Tag: 强连通分量、缩点
难度: ☆☆☆
来源: B Z O J 1051 BZOJ\ 1051 BZOJ 1051
思路: 可达点表示所有点都可到达的点。对于一个普通图来说暴力去做肯定会超时,而对于 D A G DAG DAG来说,只需要统计出度为0的点是否只存在1个。
在有向无环图中,出度为 0 0 0的点一定存在。如果存在多个,则说明这几个点之间互相是不可达的,此时不存在可达点。因此如果存在可达点,则最多存在 1 1 1个出度为0的点。
对于普通图而言,可以利用缩点,将图转换成 D A G DAG DAG,只需要求出 D A G DAG DAG中出度为 0 0 0的数目,等于1则有解。记 D A G DAG DAG中找到的可达点编号为 u u u,此时原图中可达点的数量 u u u对应原图强连通分量中点的数目。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 10000 + 10;
vector<int>G[maxn];
int pre[maxn], lowlink[maxn], sccno[maxn], dfs_clock, scc_cnt;
stack<int>S;
void dfs(int u)
{
pre[u] = lowlink[u] = ++dfs_clock;
S.push(u);
for(int i = 0; i < G[u].size(); i++)
{
int v = G[u][i];
if(!pre[v])
{
dfs(v);
lowlink[u] = min(lowlink[u], lowlink[v]);
}
else if(!sccno[v])
{
lowlink[u] = min(lowlink[u], pre[v]);
}
}
if(lowlink[u] == pre[u])
{
scc_cnt++;
for(;;)
{
int x = S.top();
S.pop();
sccno[x] = scc_cnt;
if(x == u)break;
}
}
}
//求解强连通分量
void find_scc(int n)
{
dfs_clock = scc_cnt = 0;
for(int i = 0; i < n; i++)if(!pre[i])dfs(i);
}
int chu[maxn];
int main()
{
int n, m, u, v;
scanf("%d%d", &n, &m);
while(m--)
{
scanf("%d%d", &u, &v);
u--, v--;
G[u].push_back(v);
}
find_scc(n);
//重新构图成DAG,计算每个新点的出度
for(int u = 0; u < n; u++)
{
for(int i = 0; i < G[u].size(); i++)
{
int v = G[u][i];
if(sccno[u] != sccno[v])
{
chu[sccno[u]]++;
}
}
}
//统计出度为0点的个数
int tot = 0, t;
for(int i = 1; i <= scc_cnt; i++)
if(chu[i] == 0)tot++, t = i;
int ans = 0;
if(tot == 1)//只有1个点出度为0,计算对应强连通分量中的点
{
for(int i = 0; i < n; i++)
{
if(sccno[i] == t)ans++;
}
}
printf("%d\n", ans);
return 0;
}