- 寫在前面
- A
- B
- J
- D
- C
- H
- G
- 寫在最後
寫在前面
比賽地址:https://codeforces.com/gym/514727
搬運自:ICPC 2021 ASIA YOKOHAMA REGIONAL
以下按個人向難度排序。
賽時寫三題掛三題太唐了
以及霓虹人的題解寫得什麼 b 東西根本不講程式碼實現的是吧
A
簽到。
保證每個球與兩個球相交,於是列舉每對球按照公式計算其相交體積即可。
媽的圓周率精確到小數點後 15 位才能過。
可以用 acos(-1)
替代圓周率,然而賽時真的手打了 15 位,我是麻瓜。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 110;
const double eps = 1e-9;
//=============================================================
int n;
double ans, r, with;
double x[kN], y[kN], z[kN];
double inter[kN][kN];
//=============================================================
double distance(int a_, int b_) {
return sqrt((x[b_] - x[a_]) * (x[b_] - x[a_]) +
(y[b_] - y[a_]) * (y[b_] - y[a_]) +
(z[b_] - z[a_]) * (z[b_] - z[a_]));
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> r;
for (int i = 1; i <= n; ++ i) {
std::cin >> x[i] >> y[i] >> z[i];
ans += 4.0 * acos(-1) * r * r * r / 3.0;
for (int j = 1; j < i; ++ j) {
double d = distance(i, j);
if (d >= 2.0 * r - eps) continue;
inter[i][j] = 2.0 * acos(-1) * (r - d / 2.0) * (r - d / 2.0) * (2.0 * r + d / 2.0) / 3.0;
ans -= inter[i][j];
// std::cout << i << j << " " << inter[i][j] << "\n";
}
}
std::cout << std::fixed << std::setprecision(9) << ans - with << "\n";
return 0;
}
B
列舉,貪心。
大力列舉可能的串的字尾。
Code by dztle:
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){
int x=0,f=1; char s;
while((s=getchar())<'0'||s>'9') if(s=='-') f=-1;
while(s>='0'&&s<='9') x=(x*10)+(s^'0'),s=getchar();
return x*f;
}
const int N=100005;
int n;
char s[10];
int se[105],tt[105],ok[105];
int tic[1000005];
int cnt[10006];
int q[105],top;
signed main(){
cin>>n;
for(int i=1;i<=n;++i){
scanf("%s",s+1);
int num=0,num2=0,num3=0;
for(int j=5;j<=6;++j){
num=num*10+(s[j]-'0');
}
tt[num]++;
for(int j=3;j<=6;++j){
num2=num2*10+(s[j]-'0');
}
cnt[num2]++;
se[num]=max(se[num],cnt[num2]);
for(int j=1;j<=6;++j){
num3=num3*10+(s[j]-'0');
}
tic[num3]++;
}
for(int i=0;i<=1000000;++i){
if(tic[i]==1){
ok[i%100]=1;
}
}
int ioio=0;
for(int i=0;i<=99;++i){
if(se[i]) ++ioio;
}
int ans=0;
if(ioio>=1){
for(int i=0;i<=99;++i){
if(ok[i]) ans=max(ans,(int)300000);
ans=max(ans,4000*se[i]);
}
top=0;
for(int k=0;k<=99;++k){
if(tt[k]){
q[++top]=tt[k];
}
}
sort(q+1,q+1+top);
int opop=0;
for(int k=top;k>=top-2;--k){
if(k==0) break;
opop+=q[k];
}
ans=max(ans,500*opop);
}
if(ioio>=2){
for(int i=0;i<=99;++i){
if(tt[i]){
for(int j=0;j<=99;++j){
if(i!=j&&tt[j]){
if(ok[i]) ans=max(ans,300000+4000*se[j]);
}
}
}
}
for(int i=0;i<=99;++i){
if(!ok[i]) continue;
if(tt[i]){
top=0;
for(int k=0;k<=99;++k){
if(i==k) continue;
if(tt[k]){
q[++top]=tt[k];
}
}
sort(q+1,q+1+top);
int opop=0;
for(int k=top;k>=top-2;--k){
if(k==0) break;
opop+=q[k];
}
ans=max(ans,300000+500*opop);
}
}
for(int i=0;i<=99;++i){
if(tt[i]){
top=0;
for(int k=0;k<=99;++k){
if(i==k) continue;
if(tt[k]){
q[++top]=tt[k];
}
}
sort(q+1,q+1+top);
int opop=0;
for(int k=top;k>=top-2;--k){
if(k==0) break;
opop+=q[k];
}
ans=max(ans,4000*se[i]+500*opop);
}
}
// cout<<ans<<endl;
}
if(ioio>=3){
for(int i=0;i<=99;++i){
if(!ok[i]) continue;
if(tt[i]){
for(int j=0;j<=99;++j){
if(i==j) continue;
if(tt[j]){
top=0;
for(int k=0;k<=99;++k){
if(j==k) continue;
if(i==k) continue;
if(tt[k]){
q[++top]=tt[k];
}
}
sort(q+1,q+1+top);
int opop=0;
for(int k=top;k>=top-2;--k){
if(k==0) break;
opop+=q[k];
}
ans=max(ans,300000+4000*se[j]+500*opop);
}
}
}
}
}
cout<<ans;
return 0;
}
/*
7
034207
924837
372745
382947
274637
083907
294837
*/
J
列舉,二分。
首先預處理出所有左下無其他點的點集 \(\mathbf{BL}\),右上無其他點的點集 \(\mathbf{TR}\),顯然合法點對 \((u, v)(x_u < x_v)\) 必有 \(u\in \mathbf{BL}, v\in \mathbf{TR}\)。根據性質可發現,當這兩個點集中的點按 \(x\) 排序後,\(y\) 均為遞減的。
考慮欽定 \(u\in \mathbf{BL}, v\in \mathbf{TR}\) 前提下如何判斷點對 \((u, v)\) 合法。
- 此時 \(u\) 左下 與 \(v\) 右上均沒有其他點,考慮另外兩個未被包含的區域中是否有點。
- 另外兩個未被包含的區域分別為:\((x< x_{u} \land y > y_v)\),\((x > x_v \land y < y_u)\)。
- 對於 \((x < x_u \land y > y_v)\):則應有所有 \(y > y_v\) 的點均滿足 \(x > x_u\)。
- 對於 \((x > x_v \land y < y_u)\):則應有所有 \(x > x_v\) 的點均滿足 \(y > y_u\)。
- 滿足上述兩條件則未被包含的區域中無其他點。
考慮對 \(v\in \mathbf{TR}\) 預處理 \(\operatorname{lim_x}(v) = \min\{ x | (x, y) \in \land y > y_v \}\),\(\operatorname{lim_y}(v) = \max\{ y | (x, y)\in S \land x > x_v\}\),則對於 \(u\in \mathbf{BL}\),\((u, v)\) 合法當且僅當:\(x_u< \operatorname{lim_x}(v) \land y_u< \operatorname{lim_y}(v)\),即有下圖:
由上述性質,發現若將 \(\mathbf{BL}\) 中的點按 \(x\) 遞增排序,則對於任意 \(v\in \mathbf{TR}\),能與其構成合法點對的 \(u\) 在 \(\operatorname{BL}\) 中一定構成一段連續的區間。於是在列舉 \(v\) 過程中二分求得合法區間中的點的數量即可。
預處理 \(\mathbf{BL}, \mathbf{TR}\) 即二維偏序問題,樹狀陣列預處理即可。預處理 \(\operatorname{lim_x}\) 與 \(\operatorname{lim_y}\) 均為 \(O(n)\) 級別,單次詢問合法區間時間複雜度 \(O(\log n)\) 級別。則總時間複雜度 \(O(n\log n)\) 級別。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int n;
struct Point {
int x, y, id;
} pt[kN], ptx[kN], pty[kN];
int posx[kN], posy[kN];
int xlimit[kN], ylimit[kN];
std::vector <Point> bottom_left;
LL ans;
//=============================================================
namespace BIT {
#define lowbit(x) ((x)&(-x))
const int kNode = kN;
int lim, t[kNode];
void Init(int lim_) {
lim = lim_;
memset(t, 0, sizeof (t));
}
void Insert(int pos_) {
for (int i = pos_; i <= lim; i += lowbit(i)) {
++ t[i];
}
}
int Sum(int pos_) {
int ret = 0;
for (int i = pos_; i; i -= lowbit(i)) ret += t[i];
return ret;
}
int Query(int l_, int r_ = lim) {
if (l_ > r_) return 0;
return Sum(r_) - Sum(l_ - 1);
}
}
bool cmpx(Point fir_, Point sec_) {
return fir_.x < sec_.x;
}
bool cmpy(Point fir_, Point sec_) {
return fir_.y < sec_.y;
}
void Init() {
std::sort(ptx + 1, ptx + n + 1, cmpx);
std::sort(pty + 1, pty + n + 1, cmpy);
xlimit[n] = kInf, ylimit[n] = kInf;
for (int i = n; i; -- i) {
posx[ptx[i].id] = i, posy[pty[i].id] = i;
if (i < n) {
xlimit[i] = pty[i + 1].x, ylimit[i] = ptx[i + 1].y;
xlimit[i] = std::min(xlimit[i], xlimit[i + 1]);
ylimit[i] = std::min(ylimit[i], ylimit[i + 1]);
}
}
BIT::Init(1e6 + 5);
for (int i = 1; i <= n; ++ i) {
if (!BIT::Query(1, ptx[i].y - 1)) {
bottom_left.push_back(ptx[i]);
}
BIT::Insert(ptx[i].y);
}
}
void Query(Point pt_) {
int xl = xlimit[posy[pt_.id]], yl = ylimit[posx[pt_.id]];
int posl = bottom_left.size(), posr = -1;
for (int l = 0, r = bottom_left.size() - 1; l <= r; ) {
int mid = (l + r) >> 1;
if (bottom_left[mid].x < xl) {
posr = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
for (int l = 0, r = bottom_left.size() - 1; l <= r; ) {
int mid = (l + r) >> 1;
if (bottom_left[mid].y < yl) {
posl = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
if (posl <= posr) ans += posr - posl + 1;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n;
for (int i = 1; i <= n; ++ i) {
int x, y; std::cin >> x >> y;
ptx[i] = pty[i] = pt[i] = (Point) {x, y, i};
}
Init();
BIT::Init(1e6 + 5);
for (int i = n; i; -- i) {
if (!BIT::Query(pty[i].x + 1)) {
Query(pty[i]);
// if (ans) std::cout << pty[i].x << " " << pty[i].y << "\n";
}
BIT::Insert(pty[i].x);
}
std::cout << ans << "\n";
return 0;
}
D
分治,DP。
手玩下發現,若確定了最終的樹的根節點 \(m\),則 \(m\) 為樹中高度最高的位置,且樹的形態一定是以下兩種之一:
case-1:直徑跨越根節點
case-2:直徑不跨越根節點
對於第一種情況,發現僅需要考慮左右兩側可構成的最長的鏈並將它們連向 \(m\);對於第二種情況,發現等價於將某側的一棵樹從根節點處斷開,並將兩條鏈均連向 \(m\)。發現可以透過兩側可構成的樹轉移得到以 \(m\) 為根的樹,於是考慮維護區間可構成的樹的形態並進行轉移。
設 \(\operatorname{D}(l, r, m)\) 表示由區間 \([l, r]\) 構成的根為 \(m\) 的樹的最長直徑,\(\operatorname{H}(l, r, m)\) 表示由區間 \([l, r]\) 構成的根為 \(m\) 的最長鏈的長度,初始化 \(\operatorname{D}(i, i, i) = \operatorname{H}(i, i, i) = 0\)。
根據建樹時的操作限制,需要限制狀態 \((l, r, m)\) 中滿足區間 \([l, r]\) 中 \(h_m\) 為最大值。然後考慮上述兩種情況有轉移:
則答案即為 \(\operatorname{D}(1, n, \operatorname{pos}_n)\)。
然而這轉移是 \(O(n^4)\) 的不可能跑過去啊呃呃。
發現其中有很多無用狀態。
- 對於區間 \([l, r]\) 顯然在轉移時當且僅當 \(m\) 為區間中最大值位置時,\(\operatorname{D}(l, r, m), \operatorname{H}(l, r, m)\) 才會有貢獻,於是不需要記狀態中根節點的位置,直接減一維狀態。
- 更進一步地,記 \(L_i\) 為 \(i\) 向左第一個大於 \(h_i\) 的位置,\(R_i\) 為向右第一個大於 \(h_i\) 的位置。發現以 \(m\) 為根節點的狀態中,僅有 \((L_m + 1, R_m - 1, m)\) 這一個狀態是有貢獻的,其他狀態均被包含在了該狀態中,使用其他狀態並不會使轉移更優。
發現實際上僅有 \(n\) 個狀態 \((L_i + 1, R_i - 1, i)\) 是有用的,且轉移時一定是按照 \(h\) 遞增轉移的,於是考慮按照 \(h\) 升序列舉根節點與所在區間進行轉移。可直接遞迴地分治實現。在進行區間轉移時需要求得區間內的最大值,ST 表實現即可。
僅有 \(O(n)\) 個狀態則僅需進行 \(O(n)\) 次求區間最值操作,則總時間複雜度為 \(O(n\log n)\) 級別。
實現時甚至並不需要顯式地定義上述狀態,詳見程式碼。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 1e6 + 10;
//=============================================================
int n, a[kN];
//=============================================================
namespace ST {
int mylog2[kN], f[kN][22], g[kN][22];
void Init() {
mylog2[1] = 0;
for (int i = 1; i <= n; ++ i) {
f[i][0] = a[i], g[i][0] = i;
if (i > 1) mylog2[i] = mylog2[i >> 1] + 1;
}
for (int i = 1; i <= 20; ++ i) {
for (int j = 1; j + (1 << i) - 1 <= n; ++ j) {
if (f[j][i - 1] > f[j + (1 << (i - 1))][i - 1]) {
f[j][i] = f[j][i - 1], g[j][i] = g[j][i - 1];
} else {
f[j][i] = f[j + (1 << (i - 1))][i - 1];
g[j][i] = g[j + (1 << (i - 1))][i - 1];
}
}
}
}
int Query(int l_, int r_) {
int len = mylog2[r_ - l_ + 1];
if (f[l_][len] > f[r_ - (1 << len) + 1][len]) {
return g[l_][len];
}
return g[r_ - (1 << len) + 1][len];
}
}
pii Solve(int L_, int R_) {
if (L_ > R_) return mp(-kN, -kN);
if (L_ == R_) return mp(0, 0);
int top = ST::Query(L_, R_), d = 0, h = 0;
pii retl = Solve(L_, top - 1), retr = Solve(top + 1, R_);
d = std::max(std::max(retl.first, retr.first), retl.second + retr.second + 1) + 1;
h = std::max(retl.second, retr.second) + 1;
return mp(d, h);
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
ST::Init();
pii ans = Solve(1, n);
std::cout << ans.first << "\n";
return 0;
}
C
圖論轉化。
發現這個題目的操作莫名奇妙的啊、、、需要打成目標狀態同時還要最最佳化,然而運算元量不算多,且操作後的狀態也不多、、、於是考慮能否圖論轉化,將構造最最佳化的方案,轉化為求圖上的最短路徑。
考慮記節點 \((i, j) (0\le i,j\le n)\) 表示透過某些操作可正向構造出 \(s[1:i]\),且這些操作在反向時會構造出 \(s[n-j+1:n]\),將往操作序列後新增一對 \(A_i L_i\) 視為節點間的單向邊,則答案即為構造一條從 \((0, 0)\) 到 \((n, n)\) 的字典序最小的最短路徑。
建圖時列舉節點再列舉至多 \(10^2\) 種出邊,大力討論+嘗試匹配即可,詳見程式碼。發現節點的出邊僅會連向 \(i, j\) 更大的節點,實際上是一張 DAG,在 DAG 上按操作的字典序貪心地拓撲排序即可直接求得最短路徑。
共有 \((n+1)^2\) 個節點,每個節點至多有 \(10^2\) 條出邊。時間複雜度瓶頸在於預處理時對於出邊需要大力匹配得到下一狀態,則總時間複雜度 \(O\left(10^3\times (n+1)^2\right)\) 級別。
以下程式碼因為用了 vector
存圖常數較大跑了 999ms,差點就出來了(((
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 510;
const int kInf = 1e9 + 2077;
//=============================================================
int n;
std::string s;
std::vector<int> to[kN * kN], wgt[kN * kN];
int from[kN * kN], f[kN * kN], end[kN * kN];
bool vis[kN * kN];
//=============================================================
int id(int x_, int y_) {
return x_ * (n + 1) + y_;
}
int getnext(int i_, int j_, int x_, int y_) {
if (x_ != 0 && y_ != 0 && i_ < x_) return -1;
if (x_ != 0 && y_ != 0 && n - j_ - x_ < y_) return -1;
int nexti = (x_ == 0 ? i_ + 1 : i_ + y_);
int nextj = (y_ == 0 ? j_ + 1 : j_ + x_);
if (x_ == 0 && s[i_] != (char)('0' + y_)) return -1;
for (int k = i_; k <= nexti - 1; ++ k) {
if (s[k] != s[k - x_]) return -1;
}
if (y_ == 0 && s[n - j_ - 1] != (char)('0' + x_)) return -1;
for (int k = n - nextj - 1 + 1; k <= n - j_ - 1; ++ k) {
if (s[k] != s[k - y_]) return -1;
}
return id(nexti, nextj);
}
void Init() {
for (int i = 0; i <= n; ++ i) {
for (int j = 0; j <= n; ++ j) {
f[id(i, j)] = kInf;
for (int a = 0; a <= 9; ++ a) {
for (int l = 0; l <= 9; ++ l) {
int next = getnext(i, j, a, l);
if (next == -1) continue;
to[id(i, j)].push_back(next);
wgt[id(i, j)].push_back(10 * a + l);
}
}
}
}
}
void Topsort() {
std::queue <int> q;
f[0] = 0;
q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop();
if (vis[u]) continue;
vis[u] = true;
for (int i = 0, sz = to[u].size(); i < sz; ++ i) {
if (f[to[u][i]] > f[u] + 2) {
f[to[u][i]] = f[u] + 2;
from[to[u][i]] = u;
end[to[u][i]] = wgt[u][i];
q.push(to[u][i]);
}
}
}
}
void Getans(int u_) {
if (!u_) return ;
Getans(from[u_]);
std::cout << (char)('0' + end[u_] / 10) << (char)('0' + end[u_] % 10);
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> s;
n = s.length();
Init();
Topsort();
Getans(id(n, n));
return 0;
}
/*
00000
ans: 00122100
*/
H
隨機化。
有點脫線的題。
咕咕~
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL __int128
// #define LL long long
const int kN = 110;
const int kMaxtimes = 3e5;
const char kDNA[] = "ACGT";
//=============================================================
int n, m;
std::string s[kN];
double ans1, ans2;
LL sumu[kN];
//=============================================================
LL random(LL mod_) {
return (LL)(((double) rand()) / RAND_MAX * mod_);
}
bool check(int pos_, std::string &t_) {
for (int i = 0; i < n; ++ i) {
if (s[pos_][i] != '?' && s[pos_][i] != t_[i]) return false;
}
return true;
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
srand(time(0));
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
std::cin >> s[i];
double num = 1.0;
sumu[i] = 1;
for (auto ch: s[i]) {
if (ch == '?') sumu[i] *= 4ll;
else num /= 4.0;
}
sumu[i] += sumu[i - 1];
ans1 += num;
}
for (int i = 1; i <= kMaxtimes; ++ i) {
//非等機率選取:
// int p = rand() % m + 1;
int p = std::lower_bound(sumu + 1, sumu + 1 + m, random(sumu[m])) - sumu;
std::string t = s[p];
for (int i = 0; i < n; ++ i) {
if (t[i] == '?') t[i] = kDNA[rand() % 4];
}
int flag = 1;
for (int i = 1; i < p; ++ i) {
if (check(i, t)) {
flag = 0;
break;
}
}
if (flag) ++ ans2;
}
std::cout << std::setprecision(15) << ans1 * ans2 / kMaxtimes << "\n";
return 0;
}
G
DP。
有趣的樹計數 DP。
咕咕~
寫在最後
學到了什麼:
- J:考慮不合法元素滿足的的偏序關係,觀察能否透過數學式進行表示並檢查。
- D:根據題目性質減去不合法狀態。
- C:需要達到某種目標狀態,有多種操作且直接操作過於複雜的莫名奇妙最最佳化問題,嘗試圖論轉化。
- H:大力隨機呃呃
這場題都挺有意思的,不愧是霓虹人!