A - 一道可持久化并查集好题
出题人: z h b e r zhber zhber
通过/提交: 17 / 37 17/37 17/37
题解: 此题数据极小,直接模拟即可。撤销操作就是不执行前面
k
k
k 次操作,把撤销操作视为返回
k
k
k 次之前的历史版本。用state[i][j][k]
表示第
i
i
i 次操作后第
j
j
j 个点和第
k
k
k 个点是否联通,对于
1
1
1 操作,复制上一次操作之后的状态,然后考虑新增的经过
a
−
b
a-b
a−b 边而可到达的点对。这些新点对(记为x^y
)一定是
x
x
x 在一侧,
y
y
y 在另一侧,由于新加了
a
−
b
a-b
a−b 而联通。那么分别找
a
,
b
a,b
a,b 能到达的点放到两集合内,取两个集合各一点配对都设为联通即可
O
(
n
2
)
O(n^2)
O(n2) 。或者直接floyd
传递闭包
O
(
n
3
)
O(n^3)
O(n3) 。对于第
2
2
2 种操作,复制上一次的状态,暴力找和它联通的点数。对于第三种操作,复制的不是上一次的状态,而是第
x
−
k
−
1
x-k-1
x−k−1 次的状态。时间复杂度
O
(
n
2
m
)
O(n^2m)
O(n2m) ,空间复杂度
O
(
n
2
m
)
O(n^2m)
O(n2m) 。
#include<bits/stdc++.h>
#define maxn 100 + 10
using namespace std;
int state[maxn][maxn][maxn];
int n,m;
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++) state[0][i][i]=1;
for (int i=1;i<=m;i++)
{
int op;scanf("%d",&op);
if (op==1)
{
for (int j=1;j<=n;j++)
for (int k=1;k<=n;k++)
state[i][j][k]=state[i-1][j][k];
int a,b;scanf("%d%d",&a,&b);
vector<int>A,B;A.clear();B.clear();
for (int j=1;j<=n;j++)
{
if (state[i][a][j])A.push_back(j);
if (state[i][b][j])B.push_back(j);
}
for (int j=0;j<A.size();j++)
for (int k=0;k<B.size();k++)
{
state[i][A[j]][B[k]]=1;
state[i][B[k]][A[j]]=1;
}
}else if (op==2)
{
for (int j=1;j<=n;j++)
for (int k=1;k<=n;k++)
state[i][j][k]=state[i-1][j][k];
int a,sum=0;scanf("%d",&a);
for (int j=1;j<=n;j++)if (state[i][j][a])sum++;
printf("%d\n",sum);
}else if (op==3)
{
int k;scanf("%d",&k);
for (int j=1;j<=n;j++)
for (int l=1;l<=n;l++)
state[i][j][l]=state[i-k-1][j][l];
}
}
}
B - zhb love math
出题人: s t r a w b e r r y strawberry strawberry
通过/提交: 1 / 2 1/2 1/2
题解: 考虑每个元素
b
i
b_i
bi 对答案的贡献,一定是一些包含
b
i
b_i
bi 的区间,且区间除了它以外,没有
b
j
b_j
bj 是
b
i
b_i
bi 的因数。容易想到的是,对每个
b
i
b_i
bi 找到包含它可行区间的最左边
l
i
l_i
li 和包含它可行区间的最右边
r
i
r_i
ri 那么任何区间
[
l
,
r
]
l
,
r
∈
[
l
i
,
r
i
]
[l,r]\ l,r∈[l_i,r_i]
[l,r] l,r∈[li,ri] 都是答案,这样的区间有
(
i
−
l
i
+
1
)
×
(
r
i
−
i
+
1
)
(i-l_i+1)×(r_i-i+1)
(i−li+1)×(ri−i+1) 个,朴素的
O
(
n
2
)
O(n^2)
O(n2) 想法是从
i
i
i 开始往左往右扫,找到第一个
b
j
%
b
i
=
0
b_j\%b_i=0
bj%bi=0 的
j
j
j 就是边界,因为
n
≤
1
0
5
n\leq 10^5
n≤105,是不能通过这题的。考虑怎么优化,当枚举到
i
i
i 的时候, 对于
i
i
i 左侧的
b
j
b_j
bj 如果
b
j
%
b
i
=
0
b_j\%b_i=0
bj%bi=0 那么对于位置
j
j
j 的
r
j
r_j
rj 最右只能到
(
i
−
1
)
(i-1)
(i−1) 的位置!, 因为
b
i
≤
1
0
4
b_i\leq 10^4
bi≤104 ,所以可以直接枚举
b
i
b_i
bi 的倍数
j
j
j 去更新答案,从左往右扫以 pre[i]
表示
i
i
i 最后出现的位置,对于
b
i
b_i
bi 如果出现过其倍数
j
j
j,就有 r[pre[j]]=i-1
。同理扫着扫一遍对
l
l
l 维护一个 last[i]
数组就能得到答案。时间复杂度
O
(
n
l
o
g
m
)
O(nlogm)
O(nlogm) ,空间复杂度
O
(
n
+
m
)
O(n+m)
O(n+m) 。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
const int mod = 1e9 + 7;
/* head */
int a[maxn], l[maxn], r[maxn], pre[maxn], last[maxn];
void solve() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
l[i] = 1, r[i] = n;
}
memset(pre, 0, sizeof pre);
memset(last, 0, sizeof last);
for (int i = 1; i <= n; i++) {
for (int j = a[i]; j <= 10000; j += a[i]) {
if (pre[j] != 0 && r[pre[j]] == n) r[pre[j]] = i - 1;
}
pre[a[i]] = i;
}
for (int i = n; i >= 1; i--) {
for (int j = a[i]; j <= 10000; j += a[i]) {
if (last[j] != 0 && l[last[j]] == 1) l[last[j]] = i + 1;
}
last[a[i]] = i;
}
ll ans = 0;
for (int i = 1; i <= n; i++) ans = (ans + 1LL * (i - l[i] + 1) * (r[i] - i + 1) % mod) % mod;
printf("%lld\n", ans);
}
int main() {
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
C - 旅かえる
出题人: F S M M FSMM FSMM
通过/提交: 28 / 130 28/130 28/130
题解: 将问题反过来看就是问第 n n n 片荷叶到第 i i i 片荷叶所需要花的最短时间。就是一个简单的最短路模型,我们读入时候倒着建边,从 n n n 点开始跑一遍单源最短路就是答案!时间复杂度 O ( n 3 ) O(n^3) O(n3) ,空间复杂度 O ( n + m ) O(n+m) O(n+m) 。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define maxn 100005
#define INF 0x3f3f3f3f
typedef long long ll;
using namespace std;
struct Edge
{
int to, next;
}edge[maxn*2];
int head[maxn], tot, dis[maxn];
bool vis[maxn];
void Init()
{
memset(head, -1, sizeof(head));
memset(dis, INF, sizeof(dis));
memset(vis, false, sizeof(vis));
tot = 0;
}
void add(int u, int v)
{
edge[tot].to = v;
edge[tot].next = head[u];
head[u] = tot++;
}
void spfa(int s)
{
queue<int> que;
while(!que.empty()) que.pop();
que.push(s); vis[s] = true;
dis[s] = 0;
while(!que.empty()){
int tmp = que.front();
que.pop(); vis[tmp] = false;
for (int i = head[tmp]; ~i; i = edge[i].next){
int to = edge[i].to;
if (dis[to] > dis[tmp] + 1){
dis[to] = dis[tmp] + 1;
if (!vis[to]){
que.push(to); vis[to] = true;
}
}
}
}
}
int main()
{
int T, n, x;
scanf("%d", &T);
while(T--){
scanf("%d", &n);
Init();
for (int i = 1; i <= n; i++){
scanf("%d", &x);
if (i - x >= 1) add(i - x, i);
if (i + x <= n) add(i + x, i);
}
spfa(n);
for (int i = 1; i < n; i++){
if (dis[i] == INF) printf("-1 ");
else printf("%d ", dis[i]);
}
printf("0\n");
}
return 0;
}
D - zhb love chess
出题人: c w h b b t cwhbbt cwhbbt
通过/提交: 1 / 19 1/19 1/19
题解: 考虑动态规划 f ( n o w , i , j ) f(now,i,j) f(now,i,j) 当前先手是 n o w now now 位置在 ( i , j ) (i,j) (i,j) 走到 ( 2 , m ) (2,m) (2,m) 的分数,根据他们两个人的游戏策略有:
- f ( z h b , i , j ) = a i , j + m a x { f ( z y , i − 1 , j ) , f ( z y , i , j + 1 ) } f(zhb,i,j)=a_{i,j}+max\{f(zy,i-1,j),f(zy,i,j+1)\} f(zhb,i,j)=ai,j+max{f(zy,i−1,j),f(zy,i,j+1)}
- f ( z y , i , j ) = a i , j + m i n { f ( z h b , i − 1 , j ) , f ( z h b , i , j + 1 ) } f(zy,i,j)=a_{i,j}+min\{f(zhb,i-1,j),f(zhb,i,j+1)\} f(zy,i,j)=ai,j+min{f(zhb,i−1,j),f(zhb,i,j+1)}
这个转移有些奇怪,不好直接递推,但是因为行数只有 2 2 2 ,不妨直接讨论当前位于第 i i i 列第 1 1 1 行和第 2 2 2 行,如果当前位于 ( 1 , i ) (1,i) (1,i) 那么它只可能走向 ( 2 , i ) (2,i) (2,i) 或 ( 1 , i + 1 ) (1,i+1) (1,i+1) ,而 ( 2 , i ) (2,i) (2,i) 下一步只能走到 ( 2 , i + 1 ) (2,i+1) (2,i+1) 也就是 f ( n o w , 1 , i ) f(now,1,i) f(now,1,i) 可以转化为从 f ( n o w , 1 , i + 1 ) f(now,1,i+1) f(now,1,i+1) 或 f ( n o w , 2 , i + 1 ) f(now,2,i+1) f(now,2,i+1) 转移,同理 f ( n o w , 2 , i ) f(now,2,i) f(now,2,i) 也可以转化为从 f ( n o w , 1 , i + 1 ) f(now,1,i+1) f(now,1,i+1) 或 f ( n o w , 2 , i + 1 ) f(now,2,i+1) f(now,2,i+1) 转移过来。因此最后从 m m m 到 1 1 1 倒着 D P DP DP, f ( z y , 1 , 1 ) f(zy,1,1) f(zy,1,1) 就是答案,注意边界情况。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5;
int a[2][maxn];
ll f[2][2][maxn];
int main() {
int m;
scanf("%d", &m);
for (int i = 0; i <= 1; i++) {
for (int j = 1; j <= m; j++) scanf("%d", &a[i][j]);
}
f[0][0][m] = f[1][0][m] = a[0][m] + a[1][m];
f[0][1][m] = f[1][1][m] = a[1][m];
for (int i = m - 1; i >= 1; i--) {
f[0][0][i] = a[0][i] + max(f[1][0][i + 1], f[0][1][i + 1] + a[1][i]);
f[1][0][i] = a[0][i] + min(f[0][0][i + 1], f[1][1][i + 1] + a[1][i]);
f[0][1][i] = a[1][i] + max(f[0][1][i + 1], a[0][i] + f[0][0][i + 1]);
f[1][1][i] = a[1][i] + min(f[0][1][i + 1], a[0][i] + f[1][0][i + 1]);
}
printf("%lld\n", f[1][0][1]);
return 0;
}
E - zy love banner
出题人: s a d y i 98 sadyi98 sadyi98
通过/提交: 43 / 68 43/68 43/68
题解: 读入字符串以后
O
(
n
)
O(n)
O(n) 扫一遍,维护连续zy
字串的长度最大值就是答案。
#include <bits/stdc++.h>
using namespace std;
char s[2005];
int main() {
scanf("%s", s + 1);
int mx = 0, cnt = 0;
int len = strlen(s + 1);
for (int i = 1; i <= len; i++) {
if (s[i] == 'z' && s[i + 1] == 'y') {
cnt++;
i++;
} else cnt = 0;
mx = max(mx, cnt);
}
printf("%d\n", mx);
return 0;
}
F - 新食堂
出题人: s a d y i 98 sadyi98 sadyi98
通过/提交: 28 / 86 28/86 28/86
题解: 通过分析从 n = 5 n=5 n=5 时到 n = 6 n=6 n=6 时可以发现,当每两个人之间恰好相隔一个座位时,桌子长度一直增加直到每个人之间相隔两个时都无法坐下下一个人,而之后只要一个一个的增加长度就可以增加能坐下的人数,于是我们只用分析恰好只间隔一个座位就能坐下的人数就行了。也就是分析数列 2 , 3 , 5 , 9 , 17...... 2,3,5,9,17...... 2,3,5,9,17...... 可以发现,他们之间的差值是以 2 2 2 为底的幂,而且可以根据之前的分析可以确定这个数列就是这样增加的。所以整理之后可以得到: l = a i + ( a i − 1 ) ∗ 2 + n − a i l=a_i+(a_i-1)*2+n-a_i l=ai+(ai−1)∗2+n−ai 其中 a i a_i ai 是数列中小于 n n n 的最大项。
#include <stdio.h>
int main()
{
int n;
int num[5]={0,1,3,5};
while(scanf("%d",&n)!=EOF)
{
if(n<=3)
{
printf("%d\n",num[n]);
continue;
}
int base=3,temp=2;
while(base+temp<n)
{
base+=temp;
temp*=2;
}
printf("%d\n",base*2-2+n);
}
}
G - 一道平衡树好题
出题人: z h b e r zhber zhber
通过/提交: 5 / 29 5/29 5/29
题解: 首先得意识到,每插入一个数,函数执行的次数就是在这颗平衡树上经过的边数 + 1 +1 +1 ,平衡树上的一条边,就对应于一个区间,例如节点 b i b_i bi ,假设它的直接左儿子是 a i ( a i < b i ) a_i\ (a_i< b_i) ai (ai<bi) ,那么当你询问往平衡树插入一个节点要经过多少条边的时候,实际上就是询问目前有多少个包含了它的区间。首先我们把序列做离散化,此时 1 ≤ a i ≤ n 1\leq a_i\leq n 1≤ai≤n 。
对于一个树上的点
k
k
k,只有某些数字插入时会经过它。假设根节点数字是
p
p
p,根节点左儿子数字是
q
q
q 。如果插入时经过根节点,
x
x
x 的值一定属于
[
1
,
n
]
[1,n]
[1,n] ;如果经过根节点的左儿子,
x
x
x 的值属于
[
1
,
p
−
1
]
[1,p-1]
[1,p−1];如果经过了 ”根节点左儿子” 的右儿子,
x
x
x 的值属于
[
q
+
1
,
p
−
1
]
[q+1,p-1]
[q+1,p−1]……因此每个点对应一个区间,如果新加入的数字属于这个区间就会经过它。插入一个数的时候,执行次数实际上是 “当前包含这个点的区间个数”
+
1
+1
+1,
+
1
+1
+1 是访问空子树并新建节点。同时,每加入一个点也会增加一个它对应的区间。这是一个简单的单点查询、区间修改的问题,树状数组或线段树都可以胜任。至于计算这个新点对应的区间,这个值是当前它的前驱当前它的后继
[
[
[ 当前它的前驱
+
1
,
+1,
+1, 当前它的后继
−
1
]
-1]
−1](只有属于这个区间的值才会和新点走一样的路径到这里),用 set
维护即可,可以设置两个哨兵
0
0
0 和
n
+
1
n+1
n+1 防止越界。
另外,在验题时候还有神仙发现另一种做法——把当前数的前驱和后继的插入执行次数取个max加一就是答案,大家可以试一试。
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度 O ( n ) O(n) O(n)。
树状数组版本:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
#define mx 200010
ll c[mx],ans;
void add(int x,int d){for (int i=x;i<mx;i+=i&(-i))c[i]+=d;}
ll ask(int x){ll sum=0;for (int i=x;i;i-=i&(-i))sum+=c[i];return sum;}
int n,q,cnt,a[mx];
map<int,int>mp;
set<int>s;
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)scanf("%d",a+i),mp[a[i]]=1;
for (auto i=mp.begin();i!=mp.end();i++)i->second=++cnt;
for (int i=1;i<=n;i++)a[i]=mp[a[i]];
s.insert(0);s.insert(n+1);
for (int i=1;i<=n;i++)
{
auto succ=s.lower_bound(a[i]);
auto pred=succ;pred--;
int l=(*pred)+1,r=(*succ)-1;
add(l,1);add(r+1,-1);
ans+=ask(a[i]);
s.insert(a[i]);
}
printf("%lld\n",ans);
return 0;
}
线段树版本:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5;
struct SegmentTree {
#define ls (rt<<1)
#define rs (rt<<1|1)
struct Tree {
int l, r, tag, s;
int len() { return r - l + 1; }
} t[maxn << 2];
void push_up(int rt) {
t[rt].s = t[ls].s + t[rs].s;
}
void push_down(int rt) {
if (t[rt].tag) {
if (t[ls].l != t[ls].r) t[ls].tag += t[rt].tag;
if (t[rs].l != t[rs].r) t[rs].tag += t[rt].tag;
t[ls].s += t[ls].len() * t[rt].tag;
t[rs].s += t[rs].len() * t[rt].tag;
t[rt].tag = 0;
}
}
void build(int l, int r, int rt) {
t[rt].l = l, t[rt].r = r, t[rt].tag = 0;
if (l == r) {
t[rt].s = 0;
return;
}
int m = (l + r) / 2;
build(l, m, ls);
build(m+1, r, rs);
}
void update(int l, int r, int rt) {
if (l <= t[rt].l && t[rt].r <= r) {
t[rt].s += t[rt].len();
t[rt].tag++;
return;
}
push_down(rt);
int m = (t[rt].l + t[rt].r) / 2;
if (l <= m) update(l, r, ls);
if (r > m) update(l, r, rs);
push_up(rt);
}
int query(int p, int rt) {
if (t[rt].l == t[rt].r) return t[rt].s;
push_down(rt);
int m = (t[rt].l + t[rt].r) / 2;
if (p <= m) return query(p, ls);
return query(p, rs);
}
} T;
set<int> s;
int a[maxn], b[maxn];
int main() {
int n;
scanf("%d", &n);
int up = 0, cnt = 0;
b[++cnt] = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
b[++cnt] = a[i];
up = max(up, a[i]);
}
b[++cnt] = up + 1;
sort(b + 1, b + cnt + 1);
T.build(1, cnt, 1);
s.insert(0), s.insert(up + 1);
ll ans = 0;
for (int i = 1; i <= n; i++) {
int p = lower_bound(b + 1, b + cnt + 1, a[i]) - b;
s.insert(a[i]);
auto it = s.find(a[i]);
int pre = *(--it);
++it;
int suf = *(++it);
int l = lower_bound(b + 1, b + cnt + 1, pre) - b;
int r = lower_bound(b + 1, b + cnt + 1, suf) - b;
T.update(l, p - 1, 1);
T.update(p, r, 1);
ans += T.query(p, 1);
}
printf("%lld\n", ans);
return 0;
}
H - 曜神拯救世界
出题人: k o n n y a k u konnyaku konnyaku
通过/提交: 0 / 4 0/4 0/4
题解: 首先将 n n n 种攻击按神器 A A A 需要的耐久降序排序,接下来枚举把第一件神器的耐久加到多少。当第一件神器的耐久恰能抵挡第 i i i 种攻击时,只需考虑前 i − 1 i-1 i−1 种攻击对神器 B B B 和神器 C C C 的影响。注意 1 ≤ a i , b i , c i ≤ 1 0 6 1\leq a_i,b_i,c_i\leq10^6 1≤ai,bi,ci≤106 ,所以可再维护一个数组 B [ j ] B[j] B[j] 代表当前神器 B B B 的耐久度为 j j j 时神器 C C C 的耐久度最少为多少,此时答案则为 a i + m i n { B [ j ] + j } a_i+min\{B[j]+j\} ai+min{B[j]+j}。接下来考虑当神器 A A A 的耐久度从 a i a_i ai 变为 a i + 1 a_{i+1} ai+1 时对 B B B 数组的影响:对所有 0 ≤ j < b i , B [ j ] = m a x { B [ j ] , c i } 0\leq j< b_i,B[j]=max\{B[j],c_i \} 0≤j<bi,B[j]=max{B[j],ci},该操作可用线段树区间操作来 O ( l o g n ) O(logn) O(logn) 实现;还有 m i n { B [ j ] + j } min\{B[j]+j\} min{B[j]+j} 的计算也可以用线段树来维护区间最小值,因此总的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) 。
具体实现:显然数组
B
[
j
]
B[j]
B[j] 是随着
j
j
j 的增加单调递减的,所以上文中提到的
B
[
j
]
=
m
a
x
{
B
[
j
]
,
c
i
}
B[j]=max\{B[j],c_i \}
B[j]=max{B[j],ci} 操作可变成找到一点
k
(
0
≤
k
≤
b
i
)
k(0\leq k\leq b_i)
k(0≤k≤bi) ,对所有 $k\leq j< b_i,B[j]=c_i $ ,这就成了裸的线段树区间操作;可用线段树维护该节点中
B
B
B 数组的最大值最小值以及
B
[
j
]
+
j
B[j]+j
B[j]+j 的最小值,当该节点的
m
a
x
B
≥
c
i
maxB\geq c_i
maxB≥ci 时则可以直接return
不用做任何修改;当该节点的
m
i
n
B
≤
c
i
minB\leq c_i
minB≤ci 时则可以将该节点的
m
i
n
B
,
m
a
x
B
minB,maxB
minB,maxB 均改为
c
i
c_i
ci ,
m
i
n
(
B
[
j
]
+
j
)
min(B[j]+j)
min(B[j]+j) 改为该区间左端点的下标加上
c
i
c_i
ci ,同时用
t
a
g
tag
tag 标记该节点;否则就继续往该节点的两个叶子节点进行递归修改。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 10;
/* head */
struct SegmentTree {
#define ls (rt<<1)
#define rs (rt<<1|1)
struct Tree {
int l, r, mi, mx, res, tag;
} t[maxn << 2];
void push_up(int rt) {
t[rt].mi = min(t[ls].mi, t[rs].mi);
t[rt].mx = max(t[ls].mx, t[rs].mx);
t[rt].res = min(t[ls].res, t[rs].res);
}
void push_down(int rt) {
if (t[rt].tag) {
if (t[ls].l != t[ls].r) t[ls].tag = max(t[ls].tag, t[rt].tag);
if (t[rs].l != t[rs].r) t[rs].tag = max(t[rs].tag, t[rt].tag);
t[ls].mi = t[ls].mx = t[rt].tag;
t[rs].mi = t[rs].mx = t[rt].tag;
t[ls].res = t[ls].mi + t[ls].l;
t[rs].res = t[rs].mi + t[rs].l;
t[rt].tag = 0;
}
}
void build(int l, int r, int rt) {
t[rt].l = l, t[rt].r = r, t[rt].tag = 0;
if (l == r) {
t[rt].mi = t[rt].mx = 0;
t[rt].res = l;
return;
}
int m = (l + r) / 2;
build(l, m, ls);
build(m+1, r, rs);
push_up(rt);
}
void update(int l, int r, int c, int rt) {
if (t[rt].mi >= c) return;
if (l <= t[rt].l && t[rt].r <= r && t[rt].mx <= c) {
t[rt].mi = t[rt].mx = c;
t[rt].res = c + t[rt].l;
t[rt].tag = max(t[rt].tag, c);
return;
}
push_down(rt);
int m = (t[rt].l + t[rt].r) / 2;
if (l <= m) update(l, r, c, ls);
if (r > m) update(l, r, c, rs);
push_up(rt);
}
} T;
struct node {
int a, b, c;
friend bool operator< (const node& x, const node& y) {
return x.a > y.a;
}
} p[maxn];
void solve() {
int n;
scanf("%d", &n);
int up = 0;
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &p[i].a, &p[i].b, &p[i].c);
up = max(up, p[i].b);
}
sort(p + 1, p + n + 1);
T.build(0, up, 1);
int ans = 1e9;
for (int i = 1; i <= n; i++) {
ans = min(ans, p[i].a + T.t[1].res);
T.update(0, p[i].b - 1, p[i].c, 1);
}
ans = min(ans, T.t[1].res);
printf("%d\n", ans);
}
int main() {
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
附: 本题被验题人周神仙
z
h
b
e
r
zhber
zhber 用std::set
更简短优美地实现了,大家可以尝试一下。
I - 数字集合
出题人: s a i r y o sairyo sairyo
通过/提交: 26 / 153 26/153 26/153
题解: 分两种情况考虑,若 a a a , b b b 中有 1 1 1 个为 0 0 0 ,则集合中最后只存在 a a a , b b b 两个数。若 a a a , b b b 都不为 0 0 0 。考虑集合中如果可以存在 1 1 1 和任意其他正整数则所有非零正整数都能通过 + 1 +1 +1 , − 1 -1 −1 被造出。首先可以通过辗转相减使 g c d ( a , b ) gcd(a,b) gcd(a,b) 其存在于集合中。设 a = k 1 × g c d ( a , b ) , b = k 2 × g c d ( a , b ) a=k_1×gcd(a,b),b=k_2×gcd(a,b) a=k1×gcd(a,b),b=k2×gcd(a,b) ,则可使 k 1 k_1 k1 , k 2 k_2 k2 加入集合。则可使 g c d ( k 1 , k 2 ) gcd(k_1,k_2) gcd(k1,k2) 加入集合,由于已知 g c d ( k 1 , k 2 ) = 1 gcd(k_1,k_2)=1 gcd(k1,k2)=1 ,因此可使 1 1 1 加入集合。则所有非零数都可以被造出。
#include <cstdio>
int main() {
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
if(a==0||b==0){
if(c==a||c==b) printf("YES\n");
else printf("NO\n");
} else {
if(c!=0) printf("YES\n");
else printf("NO\n");
}
return 0;
}
J - 不过都一个套路
出题人: m i a m i a o miamiao miamiao
通过/提交: 3 / 28 3/28 3/28
题解: 直接正面求复杂度是不可接受的。而区间 [ 2 , k ] [2,k] [2,k] 所有数的总和是 ( 2 + k ) × ( k − 1 ) 2 \frac{(2+k)×(k-1)}{2} 2(2+k)×(k−1) ,答案可以通过总和减去那些与 n n n 不互素的数求得。而对于一个数 i i i 如果它与 n n n 不互素,那么它的倍数 a ⋅ i a·i a⋅i 也与 n n n 不互素,而且构成以 i i i 为首项 i i i 为公差的等差数列。而发现 ∏ i = 11 18 i > 1 0 9 \prod\limits_{i=11}^{18}i>10^9 i=11∏18i>109 ,所以 n n n 的因数不会特别多,我们可以对 n n n 进行因数分解,枚举它的因子(可以通过状压或者爆搜实现) O ( 1 ) O(1) O(1) 地算出小于 k k k 以 i i i 为因子的数的总和,但是我们会把不互素的因子 i i i 和 j j j 把它们的倍数多算(比如算 2 2 2 的时候把 6 6 6 算上去了, 同理在 3 3 3 的时候也把 6 6 6 算上去了)因此还得把因子容斥一下才是最终答案。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
/* head */
vector<int> fac;
inline void solve() {
int n, k;
scanf("%d%d", &n, &k);
fac.clear();
for (int i = 2; i * i <= n; i++) {
if (n > 0 && n % i == 0) {
fac.push_back(i);
while (n && n % i == 0) n /= i;
}
}
if (n > 1) fac.push_back(n);
ll sum = 1LL * (2 + k) * (k - 1) / 2;
ll res = 0;
int sz = fac.size();
for (int i = 1; i < (1 << sz); i++) {
ll tmp = 1;
int flag = 0;
for (int j = 0; j < sz; j++) {
if (i & (1 << j)) {
flag++;
tmp *= fac[j];
}
}
int cnt = k / tmp;
if (flag & 1) res += tmp * cnt * (cnt + 1) / 2;
else res -= tmp * cnt * (cnt + 1) / 2;
}
printf("%lld\n", sum - res);
}
int main() {
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
K - 踩人
出题人: z h b e r zhber zhber
通过/提交: 5 / 18 5/18 5/18
题解: 简化一下题意就是多次询问无向图中 “点权前 k k k大的点” 的诱导子图中 a , b a,b a,b 两点是否联通。诱导子图就是选一个点集合 V ′ V' V′ ,从原图中取出 V ′ V' V′ 中的点,以及 “边两端点都在 V ′ V' V′ 内” 的边,构成的一个新图。
不难想到把所有的询问读下来,然后按照 k k k 从小到大排序离线做。那么只要每次加一个点进来,并查集维护图的信息,使得图的状态从 “点权前 k k k 大” 变成 “点权前 k + 1 k+1 k+1 大” ,就能够处理连通性询问了。那么问题的关键是怎么找出新加入一个点后,所有需要加入的新边。注意到一条边需要被加入图中时,一定是它的两端点都是点权前 k k k 大了。假设一条边 E E E 的两端点分别是前 a a a 大和前 b b b 大, E E E 会被加入图中当且仅当现在处理到点权前 m a x { a , b } max\{a,b\} max{a,b} 大。那么不妨定义一个边的点权为 “两端点的点权排名更大的那个” ,这样只要当前的 k k k 等于边权,即可加入这条边。排序尺取即可。时间复杂度 O ( n l o g n + m l o g m + t l o g t ) O(nlogn+mlogm+tlogt) O(nlogn+mlogm+tlogt) ,空间复杂度 O ( n + m + t ) O(n+m+t) O(n+m+t) 。
#include<bits/stdc++.h>
using namespace std;
struct query{int k,a,b,id,ans;}q[100010];
bool cmp1(const query &a,const query &b){return a.k<b.k;}
bool cmp2(const query &a,const query &b){return a.id<b.id;}
struct edge{int x,y,z;}e[100010];
bool operator<(const edge &a,const edge &b){return a.z<b.z;}
struct point{int v,id;}p[100010];
bool operator<(const point &a,const point &b){return a.v>b.v;}
int fa[100010];inline int getfa(int x){return fa[x]==x?x:fa[x]=getfa(fa[x]);}
int n,m,t;
int rnk[100010];
int main()
{
scanf("%d%d%d",&n,&m,&t);
for (int i=1;i<=n;i++)scanf("%d",&p[i].v),p[i].id=i;
sort(p+1,p+n+1);
for (int i=1;i<=n;i++)rnk[p[i].id]=i;
for (int i=1;i<=m;i++)scanf("%d%d",&e[i].x,&e[i].y),e[i].z=max(rnk[e[i].x],rnk[e[i].y]);
sort(e+1,e+m+1);
for (int i=1;i<=t;i++)scanf("%d%d%d",&q[i].k,&q[i].a,&q[i].b),q[i].id=i;
sort(q+1,q+t+1,cmp1);
for (int i=1;i<=n;i++)fa[i]=i;
int nm=1,nt=1;
for (int i=1;i<=n;i++)
{
while (nm<=m&&e[nm].z==i)
{
int fx=getfa(e[nm].x),fy=getfa(e[nm].y);
nm++;
if (fx==fy)continue;
fa[fy]=fx;
}
while (nt<=t&&q[nt].k==i)
{
if (q[nt].a==q[nt].b)q[nt].ans=1,nt++;else
q[nt].ans=(getfa(q[nt].a)==getfa(q[nt].b)),nt++;
}
}
sort(q+1,q+t+1,cmp2);
for (int i=1;i<=t;i++)printf("%s\n",q[i].ans?"7777777":"2333333");
return 0;
}
L - Unsuccessful hack
出题人: z h b e r zhber zhber
通过/提交: 4 / 12 4/12 4/12
题解: 注意到,对于所有长度 1...4 1...4 1...4 的小写字母字符串(共 26 + 2 6 2 + 2 6 3 + 2 6 4 = 475254 26+26^2+26^3+26^4=475254 26+262+263+264=475254 个),它们的hash值已经包括了 [ 0 , 9982 ] [0,9982] [0,9982] 一共 9983 9983 9983 个数,是所有hash的可能结果了。因此直接暴力对每个hash值记两个对应的串输出即可。记两个是为了防止输出和输入串一样被判为错,此时需要用另一个替换。
另解: 注意到这个hash有这样性质,假设 A A A 是一个长度是 3 3 3 的倍数的字符串, B B B 是任意字符串,如果 h a s h ( A ) = 0 hash(A)=0 hash(A)=0 , h a s h ( B ) = k hash(B)=k hash(B)=k ,那么 h a s h ( A + B ) = k hash(A+B)=k hash(A+B)=k 。这里 + + + 号代表的是字符串的拼接。本地暴力打表即可发现长度是 3 3 3 的hash值为0的串就有两个,分别是 z t q ztq ztq 和 s x j sxj sxj 。只要在读入串之前加上它们之一然后原封不动地输出即可。
时间复杂度 O ( T ⋅ l e n ) O(T·len) O(T⋅len) ,空间复杂度 O ( l e n ) O(len) O(len) 。
另外,在比赛中一位神仙发现了在原串后面加入444个同样字母来使得hash相等的做法,出题人并不会证明QAQ
拓展: 在取模数很大的时候,可能没有3位的hash是 0 0 0 的字符串,我们可以通过双向广搜找6位的字符串,倒着找的时候需要使用模意义下乘法逆元。取模数可以设定为非质数,那么改为使用中国剩余定理求逆或者拓展欧几里得法求逆即可。
#include<bits/stdc++.h>
using namespace std;
char s[1010];
int main()
{
int T;
scanf("%d",&T);
for (int i=1;i<=T;i++)
{
scanf("%s",s+1);
printf("sxj%s\n",s+1);
}
}