做题笔记[AGC001]

A - BBQ Easy

标签:贪心

难度:★☆☆☆☆

题目大意

将 $2n$ 个数分为 $n$ 组,每个数有值 $L_i$,每组两个数,每个数都被且只被分到一组。一组的权值是两个数值的最小值,分组方案的值就是每组值之和,求分组方案的最大值。

数据范围

$1\le n\le 100,1\le L_i\le 100$。

解题思路

考虑贪心。将数组排序后按顺序两两分组即可。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 205
int n,a[MAXN];
int main(){
cin>>n;
n*=2;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n);
int ans=0;
for(int i=1;i<=n;i+=2) ans+=a[i];
cout<<ans;
return 0;
}

B - Mysterious Light

标签:思维

难度:★★★☆☆

题目大意

有一个边长为 $N$ 的三枚镜子构成的正三角形,顶点为 $A, B, C$。有一束光线,放在 $AB$ 段的 $P$ 点上,使得 $AP=X$。这个步枪将会沿着平行于 $BC$ 的方向发射一道光。

光以直线传播,以镜子的形式反射,也会被自己的轨迹反射,当光回到初始点的时候,光被吸收。

下面的图显示了当 $N=5, x=2$ 时的光轨迹。

给定 $N$ 和 $x$,求出光线的总长度。

数据范围

$2≤N≤10^{12}$,$1≤x≤N-1$。

解题思路

把这个三角形看成被对角线分割的正方形,手动模拟长度,可以发现每一段的长度是一个辗转相减的过程,最后剩下的那一段就是 $(N,x)$。可以得到答案就是 $3(N-(N,x))$。

参考代码

1
2
3
4
5
6
7
8
9
10
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
ll n,x;
int main(){
cin>>n>>x;
cout<<3ll*(n-__gcd(x,n));
return 0;
}

C - Shorten Diameter

标签:树论、枚举

难度:★★☆☆☆

题目大意

给你一棵 $N$ 个点的无向树,定义点 $u$ 和 $v$ 之间的距离是从 $u$ 到 $v$ 的简单路径上的边数。

你需要删除一些点,使树的直径小于等于 $K$,当且仅当删除某点不会对树的联通性产生影响时才可以删除。

问至少删除多少点才可以满足要求。

数据范围

$2≤N≤2000$,$1≤K≤N-1$。

解题思路

因为 $N$ 比较小,我们考虑枚举每个点,计算以这个点为中心时,最少删去多少个点才能满足要求,具体地:

  • 若 $N$ 为偶数,我们设这个点为中心,遍历整张图,删去距离这个点超过 $k/2$ 的点。
  • 若 $N$ 为奇数,我们设这个点连接的某一条边为中心(枚举),遍历整张图,删去距离这个点超过 $\lfloor k/2 \rfloor$。

时间复杂度 $O(n^2)$。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<iostream>
#include<vector>
using namespace std;
#define MAXN 2005
int n,k,cnt,ans=0x3f3f3f3f;
vector<int> g[MAXN];
void dfs(int u,int fa,int dis){
cnt+=(dis>k/2);
for(auto v:g[u]){
if(v==fa) continue;
dfs(v,u,dis+1);
}
}
int main(){
cin>>n>>k;
for(int x,y,i=1;i<n;i++){
cin>>x>>y;
g[x].push_back(y);
g[y].push_back(x);
}
if(k&1){
for(int u=1;u<=n;u++){
for(auto v:g[u]){
cnt=0;
dfs(v,u,0);
dfs(u,v,0);
ans=min(ans,cnt);
}
}
cout<<ans;
}
else{
for(int u=1;u<=n;u++){
cnt=0;
for(auto v:g[u]){
dfs(v,u,1);
}
ans=min(ans,cnt);
}
cout<<ans;
}
return 0;
}

D - Arrays and Palindrome

标签:构造、回文串

难度:★★★★☆

题目大意

给定一个长 $m$ 的序列 $A$,和参数 $n=\sum\limits_{i=1}^m A_i$,构造两个正整数数列 $a,b$,满足:

  • $a$ 数列的数字总和是 $N$ 且是 $A$ 序列的一个排列;
  • $b$ 数列的数字总和是 $N$;
  • 如果存在某个数列 $s$ 满足以下两个条件, 则 $s$ 的所有元素必定相同:
    • 对于 $s$ 的最开始的 $a_1$ 个元素,接下来的 $a_2$ 个元素,更后面的 $a_3$ 个,等等,都构成回文串;
    • 对于 $s$ 的最开始的 $b_1$ 个元素,接下来的 $b_2$ 个元素,更后面的 $b_3$ 个,等等,都构成回文串。

数据范围

$1\le N,A_i\le 10^5,1\le M\le 100$。

解题思路

一道很好的构造题。

考虑构成一个长度为 $n$ 的回文串,会有 $n/2$ 个相等关系。而让所有元素相等,必须交叉安排两个数组,使得串首尾都留出一个接口,如下图:

不难发现,如果 $A$ 中有大于两个长度为奇数的串,就无法满足制约关系。对于合法的解,不妨将长度为奇数的串转到两边,然后让 $b$ 数组差 $a$ 的空子安排就好。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<iostream>
using namespace std;
#define MAXN 100005
int n,m,a[MAXN];
int main(){
cin>>n>>m;
int cnt=0;
for(int i=1;i<=m;i++){
cin>>a[i];
cnt+=(a[i]&1);
}
if(cnt>2){
cout<<"Impossible";
return 0;
}
for(int i=1;i<=m;i++) if(a[i]&1){
if(i!=1) swap(a[i],a[1]);
break;
}
for(int i=m;i>1;i--) if(a[i]&1){
if(i!=m) swap(a[i],a[m]);
break;
}
for(int i=1;i<=m;i++) cout<<a[i]<<" ";
cout<<endl;
if(m==1){
if(n!=1) cout<<2<<endl<<a[1]-1<<" "<<1;
else cout<<1<<endl<<1;
return 0;
}
else{
if(a[1]==1) cout<<m-1<<endl;
else{
cout<<m<<endl;
cout<<a[1]-1<<" ";
}
for(int i=2;i<m;i++) cout<<a[i]<<" ";
cout<<a[m]+1;
}
return 0;
}

E - BBQ Hard

标签:组合计数、动态规划

难度:★★★★☆

题目大意

有 $n$ 个数对 $(a_i, b_i)$,求

答案对 $10 ^ 9 + 7$ 取模。

数据范围

$2\le N\le 2\times 10^5,1\le A_i,B_i\le 2000$。

解题思路

首先考虑一个组合式 $x+y\choose x$ 的几何意义,就是从点 $(0,0)$ 走到 $(x,y)$,每一步只能向右或向上走的方案数。这个式子是可以转移的:

单独求一次可以在 $O(A_iB_i)$ 的时间求出来。现在考虑题目中的式子,也就是说对于每个 $1\le i<j\le n$,求从 $(0,0)$ 按照上述方案走到 $(a_i+a_j,b_i+b_j)$ 的方案数。

为了简化思考,我们现在改变一下表述方式:对于每个 $1\le i<j\le n$,求从 $(-a_j,-b_j)$ 走到 $(a_i,b_i)$,即向左下平移,但是大小不变。我们以 $(a_i,a_j)$ 为主元,求解其他点(负的)到这个点的方案和即可。

我们考虑动态规划。开始给每个负点 $(-a_i,-b_i)$ $1$ 的权值,设计 $f_{i,j}$ 表示从左下角走到这个点的方案数,求解即可。

注意上述状态求解了所有的 $i,j$ 的解,我们需要删去 $i,i$ 的解,通过组合计数可得这部分要删去的解的个数是 $\sum\limits_{i=1}^n{2a_i+2b_2\choose2a_i}$。因为 $i,j$ 和 $j,i$ 算重,需要将答案数除以 $2$。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<iostream>
using namespace std;
#define MAXN 200005
#define MAXA 2005
#define mod 1000000007
typedef long long ll;
ll f[MAXA<<1][MAXA<<1],n,a[MAXN],b[MAXN];
ll maxa,maxb,ans;
ll mul[MAXA<<2],inv[MAXA<<2];
int C(int n,int m){
return mul[n]*inv[m]%mod*inv[n-m]%mod;
}
ll qpow(ll a,ll b){
ll w=1;
while(b){
if(b&1) w=w*a%mod;
a=a*a%mod;
b>>=1;
}
return w;
}
void init(){
mul[0]=1;
for(int i=1;i<=4009*2;i++) mul[i]=mul[i-1]*i%mod;
inv[4009*2]=qpow(mul[4009*2],mod-2);
for(int i=4009*2-1;i>=0;i--) inv[i]=inv[i+1]*(i+1)%mod;
}
int main(){
init();
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i];
f[2005-a[i]][2005-b[i]]++;
maxa=max(maxa,a[i]);
maxb=max(maxb,b[i]);
}
for(int i=2005-maxa;i<=2005+maxa;i++){
for(int j=2005-maxb;j<=2005+maxb;j++){
f[i][j]=(f[i][j]+f[i-1][j]+f[i][j-1])%mod;
}
}
for(int i=1;i<=n;i++) ans=(ans+f[2005+a[i]][2005+b[i]])%mod;
for(int i=1;i<=n;i++) ans=(ans-C((a[i]+b[i])<<1,a[i]<<1))%mod;
ans=1ll*ans*inv[2]%mod;
ans=(ans%mod+mod)%mod;
cout<<ans;
return 0;
}

F - Wide Swap

标签:思维、拓扑排序、线段树

难度:★★★★★

题目大意

给出一个元素集合为 $\{1,2,\dots,N\}$ 的排列 $P$,当有 $i,j$ $(1\leq i<j\leq N)$ 满足 $j-i\geq K$ $(1\leq K\leq N-1)$ 且 $|P_{i}-P_{j}|=1$时,可以交换 $P_{i}$ 和 $P_{j}$。

求:可能排列中字典序最小的排列。

数据范围

$1\leq N\leq 5\times10^5$ 。

解题思路

考虑按照上述方式交换元素有什么特殊性质。

我们建立 $P$ 的反置换 $Q$,即 $P_{Q_i}=i$,将 $P_i$ 的值作为 $Q$ 的下标,$i$ 作为 $Q$ 的值。不难发现,可以交换 $Q_i$ 和 $Q_{i+1}$ 的条件是 $|Q_i-Q_{i+1}|\le K$。

通过人类的智慧思考发现,若存在一组 $1\le iK$,不管怎样交换,这两个数的相对位置不会改变

这个性质放回到 $P$ 中,等价于:$\forall 1\le i \le N,j\in(i-K,i+k),i\not=j$,若 $P_i$ 和 $P_j$ 满足偏序关系 $P_i<P_j$,则无论怎样交换,最后在 $i,j$ 位置上的数仍满足偏序关系 $P_i<P_j$。

例子 $\quad$ 对于排列 $P=\{4,5,7,8,3,1,2,6\}$,有 $P_2=5,P_3=7$,在 $K=3$ 的情况下,无论怎样交换,两个位置上的数仍满足小于关系。例如交换成 $P’=\{1,2,6,7,5,3,4,8\}$,满足 $2<6$。

这样,我们可以找出所有的偏序关系,建图后通过拓扑排序,就可以知道这些数的最小排列。

例如,我们按照大于关系见图,即找到下标 $(i-K,i+K)$ 内比 $P_i$ 小的数连边,按照拓扑顺序给下标从 $N$ 到 $1$ 赋值。

但是这个图是 $O(NK)$ 大小的,无法建图,我们利用这道题的特殊性质:每个点可能连出边的节点范围是 $(i-K,i+K)$。一开始没有入度的点就是这个范围内的最大值,可以利用线段树查找。然后在左右子区间找区间最大值,再检查是否合法即可(见代码)。

时间复杂度 $O(N\log N)$。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include<iostream>
#include<queue>
using namespace std;
#define MAXN 500005
#define mid ((tree[p].l+tree[p].r)>>1)
#define ls (p<<1)
#define rs (p<<1|1)
#define inf 0x3f3f3f3f
typedef pair<int,int> pii;
int n,k,a[MAXN],ans[MAXN];
struct node{
int l,r,maxx,pos;
}tree[MAXN<<2];
priority_queue<int> q;
void update(int p){
if(tree[ls].maxx>tree[rs].maxx){
tree[p].maxx=tree[ls].maxx;
tree[p].pos=tree[ls].pos;
}
else{
tree[p].maxx=tree[rs].maxx;
tree[p].pos=tree[rs].pos;
}
}
void build(int l,int r,int p){
tree[p].l=l,tree[p].r=r;
if(l==r){
tree[p].maxx=a[l];
tree[p].pos=l;
return;
}
build(l,mid,ls);
build(mid+1,r,rs);
update(p);
}
pii query(int l,int r,int p){
if(l>r) return {-inf,0};
if(l<=tree[p].l&&r>=tree[p].r) return {tree[p].maxx,tree[p].pos};
if(l>mid) return query(l,r,rs);
if(r<=mid) return query(l,r,ls);
pii lc=query(l,r,ls),rc=query(l,r,rs),c;
if(lc.first>rc.first) c=lc;
else c=rc;
return c;
}
void modify(int goal,int p){
if(tree[p].l==tree[p].r){
tree[p].maxx=-inf;
return;
}
if(goal<=mid) modify(goal,ls);
else modify(goal,rs);
update(p);
}
void chk(int p){
if(p==0) return;
pii now=query(max(1,p-k+1),min(p+k-1,n),1);
if(now.second==p&&now.first!=-inf) q.push(p);
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,n,1);
for(int i=1;i<=n;i++) chk(i);
int now=n;
while(q.size()){
int u=q.top();
q.pop();
ans[u]=now--;
modify(u,1);
int lpos=query(max(1,u-k+1),u-1,1).second;
int rpos=query(u+1,min(u+k-1,n),1).second;
chk(lpos);chk(rpos);
}
for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
return 0;
}