The 2024 Hunan Multi-School Programming Training Contest, Round 4

Luckyblock發表於2024-04-04

目錄
  • 寫在前面
  • 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)\),即有下圖:

image.png

由上述性質,發現若將 \(\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\) 為樹中高度最高的位置,且樹的形態一定是以下兩種之一:

1.png

case-1:直徑跨越根節點

2.png

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\) 為最大值。然後考慮上述兩種情況有轉移:

\[\begin{aligned} &\forall 1\le l\le r\le n,\ m\in [l, r],\ \forall l\le j\le r, h_m\ge h_j: \\ &\operatorname{D}(l, r, m)\leftarrow \begin{cases} \operatorname{H}(l, m - 1, m') + \operatorname{H} (m + 1, r, m'') + 2 &(h_{m'}, h_{m''} < h_m)\\ \operatorname{D}(l, m - 1, m') + 1 &(h_{m'} < h_m)\\ \operatorname{D}(m + 1, r, m'') + 1 &(h_{m''} < h_m) \end{cases}\\ &\operatorname{H}(l, r, m) \leftarrow \begin{cases} \operatorname{H}(l, m - 1, m') + 1 &(h_{m'} < h_m)\\ \operatorname{H}(m + 1, r, m'') + 1 &(h_{m''} < h_m) \end{cases} \end{aligned}\]

則答案即為 \(\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:大力隨機呃呃

這場題都挺有意思的,不愧是霓虹人!

相關文章