做题笔记[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)\),求

\[ \sum_{i=1}^{n}\sum_{j=i + 1}^{n}{a_i+b_i+a_j+b_j \choose a_i+a_j} \]

答案对 \(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)\),每一步只能向右或向上走的方案数。这个式子是可以转移的: \[ {x+y\choose x }={x+y-1\choose x}+{x+y\choose x-1} \] 单独求一次可以在 \(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 i<j\le N\),满足 \(|Q_i-Q_j|>K\),不管怎样交换,这两个数的相对位置不会改变

这个性质放回到 \(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;
}