2024“钉耙编程”中国大学生算法设计超级联赛(3)

写在前面

补题地址:https://acm.hdu.edu.cn/listproblem.php?vol=65,题号 7457~7468。

以下按个人向难度排序。

这下真当战犯了呃呃呃呃

1012

签到。

先对 \(a_2 \sim a_n\) 排个序,记其中最大的不大于 \(L\) 的数为 \(a_{p}\)

显然当 \(a_1 \ge L\) 时最优的选择是 \(a_1, a_2, a_3, a_p\),否则最优选择是 \(a_{1}, a_{2}, a_{3}, a_{n}\)

判断一下即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n, L, D, a[kN];
//=============================================================
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    std::cin >> n >> L >> D;
    for (int i = 1; i <= n; ++ i) std::cin >> a[i];
    std::sort(a + 2, a + n + 1);
    
    int flag = 1, cntL = 0;
    std::vector<int> ans; 
    ans.push_back(a[1]);
    ans.push_back(a[2]);
    ans.push_back(a[3]);
    for (auto x: ans) cntL += (x >= L);
    if (cntL >= 2) flag = 0;

    for (int i = n; i >= 4; -- i) {
      if (cntL + (a[i] >= L) < 2) {
        ans.push_back(a[i]);
        break;
      }
    }
    if (ans.size() < 4) flag = 0;

    std::sort(ans.begin(), ans.end());
    if (ans[3] - ans[0] <= D) flag = 0;

    if (flag) std::cout << "Yes\n";
    else std::cout << "No\n";
  }
  return 0;
}
/*
1
8 0 4
1 1 1 1 1 1 1 1
*/

1001

计数。

dztlb 大神手玩了下秒了,我题都没看。

发现深度自同构等价于其所有深度相同的子树均完全同构。

先考虑一棵树的情况,显然若一棵树是深度自同构的,则该树可看做由若干深度自同构的完全相同的树连到同一个根节点上构成的,于是考虑递推,设 \(f_{i}\) 表示大小为 \(i\) 的深度自同构的有根树的数量,初始化 \(f_{1} = 1\),递推时考虑连到根节点上的深度自同构的完全相同的树的大小,即有:

\[f_{i} = \sum_{d | (i - 1)} f_{d} \]

又森林可看做一棵有根树删去根节点后得到的若干子树(也相当于若干树连到一个虚拟根节点上),记 \(g_i\) 表示大小为 \(i\) 的深度自同构的有根森林的数量,同理有:

\[g_{i} = \sum_{d | i} f_{d} = f_{i + 1} \]

于是仅需依次输出 \(f_{2}, f_{3}, \cdots, f_{n + 1}\) 即可。

dztle 大神赛时写的代码里 \(a_i\) 代表上述分析里的 \(f_{i + 1}\)

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
const int mod=998244353;
int n;
int a[N];
signed main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		a[i]++;
		for(int j=i+1;j<=n;j+=(i+1)){
			a[j]+=a[i];
			a[j]%=mod;
		}
		a[i]%=mod;
		cout<<a[i]<<' ';
	} cout<<endl;
	return 0;
}
/*
1
15
1 1 1 1 1 2 2 2 2 2 3 3 3 3 3
*/

1007

线段树。

线段树典中典题了。发现数列递增相当于差分数组全正,递减相当于全负,相等相当于全 0,单峰相当于左侧全正右侧全负,则套路地考虑维护差分数组,区间修改变为单点修改,维护区间内的全正/全负/全零/单峰情况即可。

区间合并时前三者都好处理,而合并后区间单峰等价于存在下列三种情况之一:

  • 左区间全正,右区间全负。
  • 左区间单峰,右区间全负。
  • 左区间全正,右区间单峰。

对于此类合并起来比较麻烦的题,一个实现的技巧是使用结构体维护线段树节点,方便查询时把查到的区间信息进行合并,查询时直接查询合并得到的结构体即可,非常好写。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n;
LL a[kN];
//=============================================================
namespace seg {
  #define ls (now_<<1)
  #define rs (now_<<1|1)
  #define mid ((L_+R_)>>1)
  const int kNode = kN << 2;
  struct node {
    bool positive, negative, zero, mountain;
  } t[kNode];
  
  node merge(node lson_, node rson_) {
    node ret;
    ret.positive = lson_.positive && rson_.positive;
    ret.negative = lson_.negative && rson_.negative;
    ret.zero = lson_.zero && rson_.zero;
    ret.mountain = (lson_.positive && rson_.negative) || 
                     (lson_.mountain && rson_.negative) ||
                     (lson_.positive && rson_.mountain);
    return ret;
  }
  void pushup(int now_) {
    t[now_] = merge(t[ls], t[rs]);
  } 
  void build(int now_, int L_, int R_) {
    if (L_ == R_) {
      t[now_].positive = (a[L_] > 0);  
      t[now_].negative = (a[L_] < 0);
      t[now_].zero = (a[L_] == 0);
      t[now_].mountain = false;
      return ;
    }
    build(ls, L_, mid), build(rs, mid + 1, R_);
    pushup(now_);
  }
  void modify(int now_, int L_, int R_, int pos_, int val_) {
    if (L_ == R_) {
      t[now_].positive = (val_ > 0);  
      t[now_].negative = (val_ < 0);
      t[now_].zero = (val_ == 0);
      t[now_].mountain = false;
      return ;
    }
    if (pos_ <= mid) modify(ls, L_, mid, pos_, val_);
    else modify(rs, mid + 1, R_, pos_, val_);
    pushup(now_);
  }
  node query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ <= L_ && R_ <= r_) return t[now_];
    
    node ret;
    if (l_ <= mid && r_ > mid) ret = merge(query(ls, L_, mid, l_, r_), 
                                           query(rs, mid + 1, R_, l_, r_));
    else if (l_ <= mid) ret = query(ls, L_, mid, l_, r_);
    else ret = query(rs, mid + 1, R_, l_, r_);
    return ret; 
  }
  int query(int type_, int l_, int r_) {
    if (l_ == r_) return type_ != 5;

    node ret = query(1, 1, n, l_ + 1, r_);
    if (type_ == 2) return ret.zero;
    if (type_ == 3) return ret.positive;
    if (type_ == 4) return ret.negative;
    if (type_ == 5) return ret.mountain;
    return 0;
  }
}
//=============================================================
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];
  for (int i = n; i >= 2; -- i) a[i] = a[i] - a[i - 1];
  seg::build(1, 1, n);

  int q; std::cin >> q;
  while (q --) {
    int opt, l, r, x; std::cin >> opt >> l >> r;
    if (opt == 1) {
      std::cin >> x;
      a[l] += x, a[r + 1] -= x;
      if (1 < l) seg::modify(1, 1, n, l, a[l]);
      if (r + 1 <= n) seg::modify(1, 1, n, r + 1, a[r + 1]); 
    } else {
      std::cout << seg::query(opt, l, r) << "\n";
    }
  }
  return 0;
}

1011

乱搞,函数单调性。

dztlb 大神在我写傻逼线段树的时候一眼秒了,我看都没看。

周长可以表示成 \(2(\max x - \min x) + 2(\max y - \min y)\),且每个人仅会朝着正东西南北走,发现照片的两维是独立的,东西走向的人会影响 \(x\),南北走向的人会影响 \(y\),于是分别考虑上述式子两部分即可。

\(2(\max x - \min x)\) 为例,考虑每个时刻位于 \(\max x\)\(\min x\) 两个人的行动,发现该式的变化每个时刻仅有五种情况:\(-2, -1, 0, 1, 2\),且一定是关于时间的凹函数,可以直接计算或使用三分求得凹函数的极小值点即可得该式的最小值。

赛时写的什么东西这是、、、好像是直接算的极小值点并在极值点两边分别跑了一下求的最小周长、、、同机房还有模拟退火过的太搞了、、、

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+5;
const int Maxx=1e10;
int n;
int t[5];
struct node{
	int x,y;
}p[5][N],q[N];
inline bool cmp1(node a,node b){
	return a.x<b.x;
}
inline bool cmp2(node a,node b){
	return a.y<b.y;
}
int Lx=Maxx,Rx=-Maxx,Ly=Maxx,Ry=-Maxx;
int ans;
int check(int ti){
	int lx=Maxx,rx=-Maxx,ly=Maxx,ry=-Maxx;
	for(int i=1;i<=t[1];++i){
		int xx=p[1][i].x+ti,yy=p[1][i].y;
		lx=min(lx,xx); ly=min(ly,yy);
		rx=max(rx,xx); ry=max(ry,yy);
	}
	for(int i=1;i<=t[2];++i){
		int xx=p[2][i].x-ti,yy=p[2][i].y;
		lx=min(lx,xx); ly=min(ly,yy);
		rx=max(rx,xx); ry=max(ry,yy);
	}
	for(int i=1;i<=t[3];++i){
		int xx=p[3][i].x,yy=p[3][i].y-ti;
		lx=min(lx,xx); ly=min(ly,yy);
		rx=max(rx,xx); ry=max(ry,yy);
	}
	for(int i=1;i<=t[4];++i){
		int xx=p[4][i].x,yy=p[4][i].y+ti;
		lx=min(lx,xx); ly=min(ly,yy);
		rx=max(rx,xx); ry=max(ry,yy);
	}
	return rx-lx+ry-ly;
}
signed main(){
	cin>>n;
	char c;
	for(int i=1,x,y;i<=n;++i){
		cin>>x>>y>>c;
		if(c=='E'){
			++t[1];
			p[1][t[1]].x=x,p[1][t[1]].y=y;
			Ly=min(Ly,y);
			Ry=max(Ry,y);
		}
		if(c=='W'){
			++t[2];
			p[2][t[2]].x=x,p[2][t[2]].y=y;
			Ly=min(Ly,y);
			Ry=max(Ry,y);
		}
		if(c=='S'){
			++t[3];
			p[3][t[3]].x=x,p[3][t[3]].y=y;
			Lx=min(Lx,x);
			Rx=max(Rx,x);
		}
		if(c=='N'){
			++t[4];
			p[4][t[4]].x=x,p[4][t[4]].y=y;
			Lx=min(Lx,x);
			Rx=max(Rx,x);
		}
	}
	sort(p[1]+1,p[1]+1+t[1],cmp1);
	sort(p[2]+1,p[2]+1+t[2],cmp1);
	sort(p[3]+1,p[3]+1+t[3],cmp2);
	sort(p[4]+1,p[4]+1+t[4],cmp2);
	ans=check(0);
	if(Lx>=p[1][1].x){
		ans=min(ans,check(Lx-p[1][1].x));
	}
	if(Rx<=p[2][t[2]].x){
		ans=min(ans,check(p[2][t[2]].x-Rx));
	}
	if(p[1][1].x<=p[2][1].x){
		ans=min(ans,check((p[2][1].x-p[1][1].x)/2));
	}
	if(p[1][t[1]].x<=p[2][t[2]].x){
		ans=min(ans,check((p[2][t[2]].x-p[1][t[1]].x)/2));
	}
	
	if(Ry<=p[3][t[3]].y){
		ans=min(ans,check(p[3][t[3]].y-Ry));
	}
	if(Ly>=p[4][1].y){
		ans=min(ans,check(Ly-p[4][1].y));
	}
	if(p[3][1].y>=p[4][1].y){
		ans=min(ans,check((p[3][1].y-p[4][1].y)/2));
	}
	if(p[3][t[3]].y>=p[4][t[4]].y){
		ans=min(ans,check((p[3][t[3]].y-p[4][t[4]].y)/2));
	}
	
	cout<<ans*2<<'\n';
	return 0;
}
/*
5
0 2 E
0 6 S
2 0 N
2 6 S
4 4 W
*/

1008

最短路

有原图上的边铁要跑最短路了,考虑如何额外加一些边将飞飞操作也能在图上表示出来。

发现直接从 1 飞到目标点 \(u\) 仅需 \(k\times (u | 1)\) 的代价是非常好用的操作。对于一次飞飞操作 \(u\rightarrow v\),很容易证明当二进制位上为 1 的集合若有 \(s_u\notin s_v\),则一定有 \((u | v) >(1 | v)\) 劣于直接从 1 飞过去;在此基础上,又发现当 \(u, v\) 均为奇数或奇偶性不同时一定有 \((u | v)\ge (1 | v)\) 不会优于直接从 1 飞过去。于是仅需考虑 \(u, v\) 均为偶数且 \(s_u\in s_v\) 时的飞飞操作即可。

此时所有飞飞操作 \(u\rightarrow v\) 的代价均为 \(k\times (v | 1)\),仅与终点有关,考虑到子集的传递性,可得到如下的建图策略:

  • 1 向其他所有点连边 \((1, i, k\times (i | 1))\)
  • 新建虚拟点 \(n+2\sim n+n\),用于表示进行飞飞操作时节点的转移。
  • 偶数点 \(i\)\(n+i\) 连有向边 \((i, n + i, 0)\),表示在 \(i\) 开始飞飞操作。
  • 偶数点 \(n + i\)\(i\) 连有向边 \((n + i, i, k\times i)\),表示在 \(i\) 结束飞飞操作并结算代价。
  • 对于所有虚拟点 \(n+i\),由子集的传递性,枚举其二进制位 \(2^j\),连边 \((n + i - 2^j, n+i, 0)\),从而使所有虚拟点与其可以飞到的点连通。

每条新增边都对应一个 \(2\) 的幂。显然对应 \(2^j\) 的边有 \(O(\frac{n}{2^j})\) 条,则新增边的总数为 \(O(n\log n)\) 级别,然后直接跑最短路就行了。

总时间复杂度 \(O(n\log^2 n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 5e5 + 10;
const int kM = 7e6 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, m;
int edgenum, head[kN], v[kM], ne[kM];
LL k, w[kM], dis[kN];
bool vis[kN];
//=============================================================
void Add(int u_, int v_, LL w_) {
  v[++ edgenum] = v_;
  w[edgenum] = w_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;
}
void Dijkstra() {
  std::priority_queue <pr <LL, int> > q;
  for (int i = 1; i <= 2 * n; ++ i) dis[i] = kInf, vis[i] = 0;
  q.push(mp(0, 1));
  dis[1] = 0;
  
  while (!q.empty()) {
    int u_ = q.top().second; q.pop();
    if (vis[u_]) continue;
    vis[u_] = true;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      LL w_ = w[i];
      if (dis[u_] + w_ < dis[v_]) {
        dis[v_] = dis[u_] + w_;
        q.push(mp(-dis[v_], v_));
      }
    }
  }
}
void init() {
  edgenum = 0;
  for (int i = 1; i <= 2 * n; ++ i) head[i] = 0;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  
  int T; std::cin >> T;
  while (T --) {
    std::cin >> n >> m >> k;
    init();

    for (int i = 1; i <= m; ++ i) {
      int u_, v_, w_; std::cin >> u_ >> v_ >> w_;
      Add(u_, v_, w_), Add(v_, u_, w_);
    }
    for (int i = 2; i <= n; ++ i) Add(1, i, 1ll * k * (1 | i));
    for (int i = 2; i <= n; i += 2) {
      Add(i, n + i, 0), Add(n + i, i, 1ll * k * i); 
      for (int j = 0; j <= 20; ++ j) {
        if ((i >> j & 1) && (i != (1 << j))) {
          Add(n + (i - (1 << j)), n + i, 0);
        }
      }
    }
    Dijkstra();
    for (int i = 2; i <= n; ++ i) std::cout << dis[i] << " ";
    std::cout << "\n";
  }
  return 0;
}
/*
2
6 4 1
1 3 2
1 5 20
2 4 1
4 6 10
6 4 3
1 3 2
1 5 20
2 4 1
4 6 10


1
2 0 1000
*/

1003

大力讨论。

赛时少讨论了一种情况呃呃嗯吃十发也没过呃呃呃呃

有点恶心,等 dztlb 大神补了,我跑路了!

1002

启发式合并/线段树合并优化树形 DP

赛时盯着 1007 和 1003 红温了这题没去细想也是亏呃呃

求若干不相交路径价值之和,考虑求树的直径的树形 DP,首先想到一个显然的 \(O(n^2)\) DP,记 \(f_{u, c}\) 表示以 \(u\) 为根的子树中,钦定必选一条端点为 \(u\),另一端点颜色为 \(c\) 的链时,该子树可获得的最大价值,用于向上传递链的信息。记 \(g_{u}\) 表示必选一条经过根的路径时该子树可获得的最大价值。初始化 \(f_{u, c_u} = w_u\) 则有显然的转移:

\[f_{u, c} \leftarrow \sum_{v\in \operatorname{son}_u} {g_v} + \max_{v\in \operatorname{son}_u} (f_{v, c} - g_{v}) \]

\[\begin{cases} g_{u}\leftarrow \sum\limits_{v\in \operatorname{son}_u} g_{v}\\ g_{u} \leftarrow \max\limits_{1\le c\le n}\max\limits_{v_1, v_2 \in \operatorname{son_u}, v_1\not= v_2} (f_{v_1, c} - g_{v_1, c}) + (f_{v_2, c} - g_{v_2, c}) \end{cases} \]

则答案即为:

\[\max_{1\le u\le n} g_{u} \]

发现上述对 \(g\) 的转移可以在对 \(f\) 的转移同时转移,仅需在枚举儿子同时维护 \((f_{v, c} - g_{v})\) 的最大值与次大值。虽然上述状态看起来是 \(O(n^2)\) 的,但每个子树 \(u\) 中至多只有 \(\operatorname{size}_u\) 种颜色,实际上第二维的有用状态并不多,于是考虑使用 map 表示上述状态,每次转移都将 \(u, v\) 的所有状态在 map 上启发式地进行合并即可。

总时间复杂度 \(O(n\log^2 n)\) 级别。

当然线段树合并更好过,仅需令节点 \(u\) 的线段树下标为 \(c\) 的位置代表 \(f_{u, c}\) 即可,时间复杂度还能少个 \(\log n\)

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, c[kN], w[kN];
int edgenum, head[kN], v[kN << 1], ne[kN << 1];
LL ans, g[kN], delta[kN];
std::map<int, LL> f[kN];
//=============================================================
void addedge(int u_, int v_) {
  v[++ edgenum] = v_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;
}
void init() {
  std::cin >> n; 
  edgenum = 0;
  ans = -kInf;
  for (int i = 1; i <= n; ++ i) head[i] = 0, f[i].clear();

  for (int i = 1; i <= n; ++ i) std::cin >> c[i];
  for (int i = 1; i <= n; ++ i) std::cin >> w[i];
  for (int i = 1; i < n; ++ i) {
    int u_, v_; std::cin >> u_ >> v_;
    addedge(u_, v_), addedge(v_, u_);
  }
}
void dfs(int u_, int fa_) {
  LL sum = delta[u_] = g[u_] = 0;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue;
    dfs(v_, u_);
    sum += g[v_];
  }

  g[u_] = sum;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue;
    if (f[u_].size() < f[v_].size()) std::swap(f[u_], f[v_]), std::swap(delta[u_], delta[v_]);
    for (auto [col, val]: f[v_]) {
      if (f[u_].count(col)) {
        g[u_] = std::max(g[u_], sum + (f[u_][col] + delta[u_]) + (val + delta[v_]));
      }
      if (!f[u_].count(col) || 
          (f[u_][col] + delta[u_]) < (val + delta[v_])) {
        f[u_][col] = (val + delta[v_]) - delta[u_];
      }
    }
    f[v_].clear();
  }
  if (f[u_].count(c[u_])) { 
    g[u_] = std::max(g[u_], sum + (f[u_][c[u_]] + delta[u_]) + w[u_]);
    if ((f[u_][c[u_]] + delta[u_]) < w[u_]) f[u_][c[u_]] = w[u_] - delta[u_];
  } else {
    f[u_][c[u_]] = w[u_] - delta[u_];
  }
  delta[u_] += sum - g[u_];
  ans = std::max(ans, g[u_]);
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    init();
    dfs(1, 0);
    std::cout << ans << "\n";
  }
  return 0;
}

写在最后

参考:https://www.cnblogs.com/cjjsb/p/18326139

学到了什么:

  • 1008:造极限数据卡。
posted @ 2024-07-27 01:08  Luckyblock  阅读(424)  评论(1编辑  收藏  举报