A. 字符画
知识点
思维、模拟。
大致思路
从左到右竖着遍历每一列,当遍历到 '#' 时依次查看当前字符是否是字符 2 -> 字符 3 -> 字符 1,如果是则把字符用'.'覆盖掉。
我们可以发现这种遍历方式可以唯一区分这三种字符,同时字符画本身又是合法,那么最终总能把所有字符覆盖掉,只需覆盖时计算即可。
此外,判断字符顺序并不唯一,同时也有从下到上的遍历方式也能得到正确答案。
参考代码
#include<bits/stdc++.h>
using namespace std;
vector<vector<pair<int, int>>> Char = {
{
{0, 0}, {0, 1}, {0, 2},
{1, 0}, {1, 2},
{2, 0}, {2, 1}, {2, 2},
{3, 0},
{4, 0}
},
{
{0, 0}, {0, 1}, {0, 2},
{1, 2},
{2, 1}, {2, 2},
{3, 2},
{4, 2}
},
{
{0, 0}, {0, 1}, {0, 2},
{1, 0}, {1, 2},
{2, 0}, {2, 1}, {2, 2},
{3, 0}, {3, 2},
{4, 0}, {4, 1}, {4, 2}
}
};
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<string> s(n);
for(int i = 0; i < n; i++ ){
cin >> s[i];
}
vector<int> ans(3);
auto print = [&](int i, int j, int id){
for(auto [dx, dy] : Char[id]){
int x = i + dx, y = j + dy;
s[x][y] = '.';
}
ans[id] += 1;
};
for(int i = 0; i < m; i++ ){ //遍历列
for(int j = 0; j < n; j++ ){ //竖着遍历每一列
if(s[j][i] != '#'){
continue;
}
// 先判断字符2
if(s[j + 1][i] != '#'){
print(j, i, 1);
continue;
}
// 再判断字符3和字符1
bool ok = true;
for(auto [dx, dy] : Char[2]){
int x = j + dx, y = i + dy;
if(s[x][y] != '#'){
ok = false;
}
}
print(j, i, (ok ? 2 : 0));
}
}
for(int i = 0; i < 3; i++ ){
cout << ans[i] << " \n"[i == 2];
}
return 0;
}
B. A+B Problem
知识点
,根号分治。
大致思路
先考虑一个暴力做法,设 表示数字 在数组 中的出现次数,对于询问到的每一个 ,求 即为当前询问的答案,单次询问的时间复杂度 ;
一共有 次询问,最坏情况下每次询问的 都不一样,相当于对于每一个 ,我们都需要求一次 ,总的时间复杂度 ;
考虑优化求和式子,用 枚举数字的出现次数来将式子中的 转换掉,这样一来,对于每一个 ,我们需要求的就是:;
令 ,则可以将 转换为 ,不难发现这是一个卷积的形式,使用 即可在 的时间复杂度内计算该求和式子;
再注意到 ,有一个经典性质:不同的 种数至多只有 种;
因此,在式子 中, 至多只有 种不同的取值,整个式子可以在 的时间复杂度内计算;
考虑继续优化,设置阈值 ,对 的大小进行分别处理:
①:当 时,使用上述式子 来计算,时间复杂度 ;
②:当 时,满足这个条件的不同数字个数不超过 个,于是可以通过 的时间复杂度暴力枚举两个数字 来计算,即 ,注意这里减 是为了减去 时数字 的贡献;
令 ,则当 时,复杂度达到最优;
相比于 ② 的暴力枚举,① 的 的常数较大,可以将 适当降低以达到更快的程序运行效率;
总体时间复杂度为 。
输出答案的时候不要忘记除 。
参考代码
#pragma GCC optimize(2)
#include <bits/stdc++.h>
using namespace std;
#define vv vector
#define ll int
struct comp{
double x,y;
comp(double _x=0.0,double _y=0.0){x=_x,y=_y;}
comp operator-(const comp&b)const{return comp(x-b.x,y-b.y);}
comp operator+(const comp&b)const{return comp(x+b.x,y+b.y);}
comp operator*(const comp&b)const{return comp(x*b.x-y*b.y,x*b.y+y*b.x);}
};
const double PI=acos(-1.0);
void change(vector<comp>&f,ll len){
for(ll i=1,j=len/2,k;i<len-1;++i){
if(i<j)swap(f[i],f[j]);
k=len/2;
while(j>=k)j-=k,k/=2;
if(j<k)j+=k;
}
}
void fft(vector<comp>&f,ll len,ll op) {
change(f,len);
for(ll i=2;i<=len;i<<=1){
comp wn(cos(2*PI/i),sin(op*2*PI/i));
for(ll j=0;j<len;j+=i){
comp w(1,0);
for(ll k=j;k<j+i/2;++k){
comp u=f[k],t=w*f[k+i/2];
f[k]=u+t,f[k+i/2]=u-t;
w=w*wn;
}
}
}
if(op==-1)
for(ll i=0;i<len;++i)
f[i].x/=len;
}
void polymul(vector<ll> &f){
ll len=1,deg=2*f.size()+1;
while(len<deg)len<<=1;
vector<comp> a;
f.resize(len),a.resize(len);
for(ll i=0;i<len;++i)a[i]={1.0*f[i],0.0};
fft(a,len,1);
for(ll i=0;i<len;++i)a[i]=a[i]*a[i];
fft(a,len,-1);
for(ll i=0;i<len;++i)f[i]=(ll)(a[i].x+0.5);
f.resize(deg);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
ll n,m,q;cin>>n>>m>>q;
ll B=pow(m/__lg(m),0.25);
vv<ll> c(m+1);
for(ll i=1;i<=n;++i){
ll x;cin>>x;
++c[x];
}
vv<ll> ans(2*m+1);
//<B
for(ll i=1;i<=B;++i){
vv<ll> a(m+1);
for(ll j=1;j<=m;++j){
if(c[j]>=i)a[j]=1;
}
polymul(a);
for(ll j=1;j<=2*m;++j){
ans[j]+=a[j];
}
}
//>B
vv<ll> b;
for(ll i=1;i<=m;++i){
if(c[i]>B)b.push_back(i);
}
for(ll i:b){
for(ll j:b)
ans[i+j]+=min(c[i],c[j])-B;
}
for(ll i=1;i<=2*m;++i){
ans[i]/=2;
}
while(q--){
ll x;cin>>x;
cout<<ans[x]<<"\n";
}
return 0;
}
题外话
来自 ;
第一眼看上去就感觉很毒瘤的复杂度;
C. 一日之计在于晨
知识点
扩展欧几里得( )。
大致思路
题意等价于求出绝对值最小的 使得 的值最小;
令 ,问题等价于求一组 使得 最小;
移项得 ,根据裴蜀定理可知,若有解,则必须满足 ;
那么就有 ;
即 ,那么有 ;
显然当 时, 取得最小值,此时有 ;
将 带入 可得 ;
用 求解出最小的正整数解和最大的负整数解即可。
参考代码
#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define ll long long
#define int long long
using namespace std;
int exgcd(int a, int b, int &x, int &y){
if(b == 0){
x = 1;
y = 0;
return a;
}
int d = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - (a / b) * y;
return d;
}
void solve(){
int t, a, m;
cin >> t >> a >> m;
int x, y;
int d = exgcd(a, m, x, y); // ax + my = d
t = t - 1;
t = ((t % d - t) % m + m) % m;// ax + my = t % d - t
x = x * t / d;
x = (x % (m / d) + (m / d)) % (m / d);
cout << min(x, m / d - x) << '\n';
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int T = 1;
cin >> T;
while(T--){
solve();
}
}
D. qfl_zzz的mex
知识点:
双指针、队列。
大致思路:
先考虑写一个暴力的做法,枚举左端点 ,然后从 的位置开始往右边枚举右端点 ,在这个过程中可以动态地维护 的值,然后累加答案,时间复杂度 ;
注意到有这样一个性质:若区间 的 为 ,则 必定出现在区间 中;
设区间元素和为 ,则有 ,于是有 ;
至此,不难发现,在枚举右端点 的过程中, 至多能改变 次,因此,我们只需要关注 在左端点 之后的首次出现位置;
对于区间和小于 的限制,可以使用双指针来解决;
对于 的首次出现位置,可以在双指针的过程中用队列动态地维护;
对于每个左端点 ,通过 的复杂度枚举 值发生变化的右端点 的位置,然后统计答案即可;
时间复杂度 。
参考代码;
#pragma GCC optimize(2)
#include <bits/stdc++.h>
using namespace std;
#define vv vector
#define ll long long
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
ll n,x;cin>>n>>x;
vv<ll> a(n+1);
for(ll i=1;i<=n;++i)cin>>a[i];
ll m=sqrt(2*x)+5,ans=0,sum=0;
vv<queue<ll>> q(m+1);
for(ll l=1,r=0;l<=n;++l){
while(l>r||(r<=n&&sum<=x)){
if(++r>n)break;
sum+=a[r];
if(a[r]<=m){
q[a[r]].push(r);
}
}
ll pre=l;
for(ll i=0;i<=m;++i){
if(q[i].size()==0){
if(pre<r)ans+=i*(r-pre);
break;
}
ll next=q[i].front();
if(next>pre){
ans+=i*(next-pre);
pre=next;
}
}
sum-=a[l];
if(a[l]<=m){
q[a[l]].pop();
}
}
cout<<ans<<"\n";
return 0;
}
题外话
卡不满 的做法,在验题时还被 的做法爆了标程(悲
E. 数星星
知识点
序,前缀 。
大致思路
要判断一个子树内是否存在相同的颜色,我们可以很套路地用 序将子树表示为区间;
于是问题就变为了:给定 个区间,判断区间内是否存在相同颜色的两个点;
我们可以按照 序从 到 的顺序枚举每个点,同时维护下面两个值:
表示 序为 的点对应的颜色的上一次出现位置对应的 序;
表示 序在 中的所有点的最大的上一次出现位置对应的 序,即 ;
令 表示某棵子树对应的 序区间,若要判断区间内是否存在相同的颜色,即判断区间内是否存在一个位置 使得 ;
由于当 时,显然有 ,所以,判断区间 等价于判断前缀 ;
至此,对于每一个子树对应的区间 ,我们直接判断是否满足 即可;
对于 的值,我们可以在 的过程中动态地维护;
时间复杂度 。
参考代码
#include <bits/stdc++.h>
using namespace std;
int n,dfn,ans,mx;
vector<int> a,id;
vector<vector<int>> e;
void dfs(int u){
int l=++dfn;
mx=max(mx,id[a[u]]);
id[a[u]]=dfn;
for(int v:e[u])dfs(v);
int r=dfn;
if(mx<l)++ans;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int n;cin>>n;
a.resize(n+1);
id.resize(n+1);
e.resize(n+1);
for(int i=1;i<=n;++i)cin>>a[i];
for(int i=2;i<=n;++i){
int x;cin>>x;
e[x].push_back(i);
}
dfs(1);
cout<<ans<<"\n";
return 0;
}
F. 最长严格上升子序列和
知识点
数位 ,最长上升子序列。
大致思路
考虑数位 。
令 :表示当前枚举的前缀是否全为前导 ,是否有上界限制,此时枚举第 位,前缀数码的状态记录为 的所有可表达数字 的 之和;
下面探讨 的定义方式:
定义一个长度为 的数组来压缩已遍历的前缀信息(长度只需要定义为 ,因为只有 种数字的情况下,最长严格上升子序列的长度至多为 );
定义 :表示严格上升子序列的长度为 的最小尾部元素的值;
数组满足以下形式:
①:数组总存在前缀严格连续上升的。数组后缀总是全为 ;
②:总体上数组的形式为,严格严格且连续上升的前缀 全 的后缀;
容易看出该数组一共有 种形式,证明如下:
①:不妨枚举前缀一共有多少种数字,对于 种数字前缀,容易看出一共有 种情况;
②:集合根据 得到一个划分;
③:因此数组的形式总数为 ;
基于上述观察,使用的一些trick(技巧)
①:通过 进制可以将上述数组状态压缩表示, 用来表示无穷大;
②:由于压缩的状态是离散的,考虑用 存储转移;
状态转移:
①:综合考虑当前 , 的标记情况确定可枚举数字的范围后,枚举当前数位上放置的数字 ;
②:更新 的步骤:
-对于 从后往前遍历 :如果则 ;否则,结束遍历;
-对于 ,
时间、空间复杂度分析:
空间复杂度:
,参照 ;
时间复杂度:
①: 查询,总查询次数约为空间总数,每次查询的话费为 ; ②:转移花费,计算每个状态的花费为 ; ③:最终规模可以控制在 ;
参考代码
#include<bits/stdc++.h>
using namespace std;
#define all(x) (x).begin(),(x).end()
const int mod = (int)1E9 + 7;
long long p11[15];
void init() {
p11[0] = 1;
for (int i = 1; i < 11; i++) {
p11[i] = p11[i - 1] * 11LL;
}
}
void add(int& x, int y) {
x += y;
if (x >= mod) x -= mod;
}
array<int, 10> number_array(long long x) {
array<int, 10> res;
// long long tx = x;
for (int i = 0; i < 10; i++) {
res[i] = x % 11;
x /= 11;
}
return res;
}
long long array_number(array<int, 10> dig) {
long long res = 0;
for (int i = 0; i < 10; i++) {
res += dig[i] * p11[i];
}
for (int i = 1; i < 10; i++) {
if (dig[i] < dig[i - 1]) {
cout << res << "\n";
}
}
return res;
}
vector<int> d;
map<long long, int> f[2][2][110];
int dfs(int zero, int limit, int cur, long long pre) {
if (cur == -1) {
return pre / p11[9];
}
if (f[zero][limit][cur].count(pre)) {
return f[zero][limit][cur][pre];
}
int& res = f[zero][limit][cur][pre] = 0;
int mx = limit ? d[cur] : 9;
array<int, 10> t;
for (int i = 0; i <= mx; i++) {
t = number_array(pre);
bool flag = true;
if (i == 0) {
if (t[i] == 1) flag = false;
else if (zero == false) t[0] = 1;
}
else {
if (t[i] == t[i - 1] + 1) flag = false;
else t[i]++;
}
if (flag) {
for (int j = i + 1; j <= 9 && t[j] < t[i]; j++) {
t[j] = t[i];
}
}
add(res, dfs(zero & (i == 0), (i == mx) & limit, cur - 1, array_number(t)));
}
return res;
}
long long solve(string x) {
for (auto s : x) {
d.push_back(s - '0');
}
reverse(all(d));
return dfs(true, true, d.size() - 1, 0);
}
signed main() {
init();
string r;
cin >> r;
cout << solve(r) << "\n";
}
G. 能赢吗?会赢的!
知识点
二分。
大致思路
如果能力值 能够战胜所有 boss,那么显然对于 的能力值 也能够战胜所有 boss;
答案具有单调性,二分答案即可;
参考代码
#include<iostream>
#include<vector>
using namespace std;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<long long> a(n + 2);
for(int i = 1; i <= n; i++){
cin >> a[i];
}
auto check = [&](long long x){
auto b = a;
for(int i = 1; i <= n; i++){
if(x <= b[i]){
return false;
}
x += a[i] / 2;
b[i + 1] += (a[i] + 1) / 2;
}
return true;
};
long long low = 1 , high = 1E9 + 10;
while(low < high){
long long mid = (low + high) / 2;
if(check(mid)){
high = mid;
}else{
low = mid + 1;
}
}
cout << low << "\n";
}
H. NING-NING-chatgpt
知识点
模拟。
大致思路
签到题,模拟即可,注意对空格的处理。
参考代码
#include <bits/stdc++.h>
using namespace std;
char p[26]={'4','6','c','d','3','f','9','h','i','j','k','1','m','n','0','p','q','r','5','7','u','v','w','x','y','2'};
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
map<char,char> mp;
for(int i=0;i<26;++i)mp[p[i]]=char(i+'a');
string s;
getline(cin,s);
for(int i=0;i<s.size();++i)
if('0'<=s[i]&&s[i]<='9')cout<<mp[s[i]];
else cout<<s[i];
return 0;
}
I. 不是莫比乌斯反演,不是欧拉反演
知识点
容斥,数论,组合数学,。
大致思路
设 为满足 的子集个数;
则答案 ;
考虑怎么去求每一个 的值,不难想到一个暴力的做法;
用 暴力枚举 中的每一个数字,用 枚举 的值,对于每一个 都有两种选择:
①:不考虑加入子集,则有转移方程 ;
②:考虑将其加入子集,则有转移方程 ;
至此,我们有一个 的一个做法,其中 表示 的因子个数;
考虑优化,不难发现有这样一个性质: ;
因此,若存在两个不同的整数 满足 ,我们显然可以在 转移的时候,将 作为同一种数字去考虑;
注意到 的值至多只有 种,这启发我们可以通过将所有 按照 的值进行分类,来优化 转移的过程中枚举 所耗费的复杂度;
进一步地,设 表示 中满足 的 的个数,可以通过枚举倍数和容斥来计算所有的 ,即 ;
接下来,考虑怎么转移:
用 枚举 的值,用 枚举 的值,用 枚举选择多少个 ,则对于每一个 都有两种选择:
①:不考虑将 加入子集,则有转移方程 ;
②:考虑将 加入子集,则有转移方程 ;
的数量级是 的,此时的时间复杂度仍然是 ,不足以通过本题;
注意到,若 ,则有 ,进而有 ;
也就是说,在从小到大枚举 的过程中 的值至多会连续变化 次,设 ,则枚举 的复杂度降为了 ;
而对于 的部分,则有转移 ,由于 较大,记得递推预处理组合数;
至此,总体时间复杂度 。
参考代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll mod=1e9+7;
ll qpow(ll a,ll b){
ll res=1;
while(b>0){
if(b&1)res=res*a%mod;
a=a*a%mod,b>>=1;
}
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cin.tie(0);
ll l,r,n;cin>>l>>r>>n;
vector<ll> d;
for(ll i=1;i<=n;++i){
if(n%i==0)d.push_back(i);
}
vector<ll> cnt(d.size());
for(ll i=d.size()-1;i>=0;--i){
cnt[i]=r/d[i]-(l-1)/d[i];
for(ll j=i+1;j<d.size();++j){
if(d[j]%d[i]==0)cnt[i]-=cnt[j];
}
}
vector<ll> inv(__lg(n)+1);
for(ll i=1;i<=__lg(n);++i){
inv[i]=qpow(i,mod-2);
}
vector<ll> id(n+1);
for(ll i=0;i<d.size();++i){
id[d[i]]=i;
}
vector<ll> dp(d.size());
dp[0]=1;
for(ll i=0;i<d.size();++i){
ll m=min(cnt[i],__lg(n));
vector<ll> c(m+1,1);
for(ll j=1;j<=m;++j){
c[j]=c[j-1]*inv[j]%mod*(cnt[i]-j+1)%mod;
}
ll tot=qpow(2,cnt[i]);
vector<ll> f(d.size());
for(ll j=0;j<d.size();++j){
ll x=d[j],rest=tot;
for(ll k=0;k<=m;++k){
(f[id[x]]+=c[k]*dp[j]%mod)%=mod;
rest=(rest-c[k]+mod)%mod;
x=__gcd(x*d[i],n);
}
(f[id[x]]+=rest*dp[j]%mod)%=mod;
}
dp=f;
}
ll ans=0;
for(ll i=0;i<d.size();++i){
ans=(ans+dp[i]*d[i]%mod)%mod;
}
cout<<ans<<"\n";
return 0;
}
题外话
来自 ;
被他推荐写这题的时候,红温了快三个小时(悲
J. 矩阵最大路径与
知识点
贪心,拆位,搜索。
大致思路
定义最后的答案为 ,对 关注其二进制,我们可以很套路地拆位,然后从高到低确定每一位:
①:对于第 位,如果存在一条合法路径使得路径上所有的数字 。那么 在第 位上的值也必须为 ;
②:对于第 位,在满足第 位最优的前提下,继续尝试寻找一条合法路径使得路径上所有的数字 ;
③:若找不到,则将所有满足 的合法路径作为下一位需要考虑的路径;
④:按照从高到低的顺序依次处理每一个二进制位即可。
参考代码
#include<vector>
#include<iostream>
#include<array>
#include<queue>
using namespace std;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
vector<vector<int>> a(n + 1, vector<int>(m + 1));
vector<vector<int>> vis(n + 1, vector<int>(m + 1));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
}
int ans = 0;
for (int i = 29; i >= 0; i--) {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
vis[i][j] = 0;
}
int tag = ans + (1 << i);
queue<array<int, 2>> que;
if ((a[1][1] & tag) == tag) {
que.push({ 1 , 1 });
vis[1][1] = 1;
}
int dx[] = { 0 , 0 , 1 , -1 };
int dy[] = { 1 , -1 , 0 , 0 };
while (que.size()) {
array<int, 2> t = que.front();
int x = t[0], y = t[1];
que.pop();
for (int i = 0; i < 4; i++) {
int tx = x + dx[i];
int ty = y + dy[i];
bool flag = (tx >= 1) && (tx <= n) && (ty >= 1) && (ty <= m) && (vis[tx][ty] == 0);
if (flag) {
if ((a[tx][ty] & tag) == tag) {
vis[tx][ty] = 1;
que.push({ tx , ty });
}
else vis[tx][ty] = 2;
}
}
if (vis[n][m] == 1) {
ans |= (1 << i);
}
}
}
cout << ans << "\n";
}
K. gzhu is all you need!
知识点
模拟。
大致思路
从头到尾枚举一遍 个城市,每到达一个城市,先判断一下力量值 是否大于 ;
若是,就消耗一点力量值,然后无代价地通关当前城市并忽略掉当前城市的字母;
否则,令代价 并将当前城市的字母收集起来,收集之后判断一下当前是否已经收集了 'g' 'z' 'h' 'u' 这四个字母;
若已集齐四个字母,则更新一下力量值 ,然后将所有收集到的字母丢弃;
按照以上顺序逐个处理每个城市即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int n;cin>>n;
string a;cin>>a;a=" "+a;
int ans=0,x=0,sum=0;
map<char,int> cnt;
for(int i=1;i<=n;++i){
if(x>0)--x;
else{
++ans;
if(a[i]=='g'||a[i]=='z'||a[i]=='h'||a[i]=='u'){
++cnt[a[i]],++sum;
}
if(cnt.size()==4){
x=sum,cnt.clear(),sum=0;
}
}
}
cout<<ans<<"\n";
return 0;
}
L.qfl_zzz的树上距离
知识点:
最近公共祖先( ),调和级数。
大致思路:
我们可以先假设,对于所有的 都有 ,这样便可以先计算出来 的值,然后再减去 时的贡献,即是正确的答案;
对于 的计算是比较容易的:我们可以单独地考虑每一条边的对答案的贡献,即将 这条边断开后,点 所在的连通块的大小与点 所在连通块的大小的乘积(记得再乘 );
接下来考虑怎么减去 时的贡献;
注意到,对于任意两点 都有 ,若 还满足 ,则必然有 ;
至此,我们可以枚举点 ,然后再枚举满足 条件的点 ,以此来得到 和 的值,若满足 ,则令答案 ;
枚举 的时间复杂度 ,可以通过调和级数来证明;
对于某一对 求 则可以通过 的时间复杂度求最近公共祖先( )以及预处理结点深度来计算;
总体时间复杂度 。
参考代码:
#pragma GCC optimize(2)
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int n;cin>>n;
vector<vector<int>> e(n+1);
for(int i=1;i<=n-1;++i){
int u,v;cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
long long ans=0;
vector<int> sz(n+1),son(n+1),top(n+1),fa(n+1),dep(n+1);
function<void(int,int)> dfs1=[&](int u,int pre){
sz[u]=1;
fa[u]=pre;
dep[u]=dep[pre]+1;
int mx=-1;
for(int v:e[u]){
if(v==pre)continue;
dfs1(v,u);
ans+=(long long)2*sz[v]*(n-sz[v]);
sz[u]+=sz[v];
if(mx<sz[v]){
son[u]=v;
mx=sz[v];
}
}
};
dfs1(1,0);
function<void(int,int)> dfs2=[&](int u,int tp){
top[u]=tp;
if(!son[u])return;
dfs2(son[u],tp);
for(int v:e[u]){
if(v==fa[u]||v==son[u])continue;
dfs2(v,v);
}
};
dfs2(1,1);
auto lca=[&](int u,int v){
while(top[u]!=top[v]){
if(dep[top[u]]>dep[top[v]])u=fa[top[u]];
else v=fa[top[v]];
}
return dep[u]>dep[v]?v:u;
};
auto dist=[&](int u,int v){
return dep[u]+dep[v]-2*dep[lca(u,v)];
};
for(int i=1;i<=n;++i)
for(int j=1;i*j<=n;++j){
int dis=dist(i,j);
if(i*j<dis)ans+=i*j-dis;
}
cout<<ans<<"\n";
return 0;
}
题外话
本题原本是 版本的启发式合并+树状数组,后来验题人反应整场题目有点偏难,于是就换成了 版本的了(悲
M. 计算几何
知识点
构造。
大致思路
给出一个比较显然的构造思路:
将点的坐标存入 ,然后排序,先比较第一维 ,如果相等进而比较第二维 ;
基于上述的偏序关系,采取以下连点策略:
将排好序之后的点,两两配对相连,即对于每一个奇数 ,将点 与点 相连;
这种方法保证了线段的数量为 ,且任意两条线段之间是不会相交。
证明如下:
可以通过反证法证明:
①:假设上述方式中,存在情况 与 相交的情况。
②:对点 按照上述规则进行排序。
③:不妨假设排序后, 点处于第一位。
关注 之间的位置, 两点中必定存在一点 ,满足 ;
如果 ,亦会存在;
否则,两线段不会相交。
同时这种现象和我们的构造策略相矛盾,因为在上述策略中 应该连接 ,而不是 。
参考代码
#include<iostream>
#include<vector>
#include<array>
#include<algorithm>
using namespace std;
#define all(x) (x).begin(),(x).end()
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<array<int , 3>> arr;
for(int i = 0; i < n; i++){
int x , y ;
cin >> x >> y;
arr.push_back({y , x , i + 1});
}
sort(all(arr));
cout << n / 2 << "\n";
for(int i = 1; i < n; i += 2){
cout << arr[i][2] << " " << arr[i - 1][2] << "\n";
}
}