ACM散题习题库5【持续更新】

终于更新到5了,但是发现并不是做过的题仍然记得,所以现在应该着重记录一些相对简单且模板的题目了。

 

 

501. H - Clock HDU - 6551【环上点覆盖 问题】

题意:给你一个环[0,N-1],和一个起始点S,同时还有n个在环上的点,请你求出最短的时间从S出发,去覆盖这n个点。

解决这个环问题的关键在于拆环。拆环的关键在于确定拆环的点,然后把这个点当作原点O。然后就可以从序列的角度去写for循环了,对我来说这样容易理解。

定义s到第i个点的顺时针距离为di,我们把n个点根据d排序,就可以得到一个序列啦!然后就枚举ii+1这两个点作为两个端点,si的距离就是di,到i+1的距离是Ndi+1。那么答案就是: 2min(di,Ndi+1)+max(di,Ndi+1)。然后记得特判一下首尾就行了。

 

502. J - Tangram HDU - 6553【小学奥数 + 切蛋糕题目】【题解

理解:在一个空白平面,一条直线把平面划分为2,平面数量+1。

然后以后的每一条直线都和前面的k-1直线相交,平面数量加k。主要想清楚为什么一条直线和其它k条直线相交会多出k+1个平面,这道题就好做了。

 

503. 2019CCPC女生赛-Union【搜索计数】

题目给定了7个条件,直接考虑这些条件确实不好计数。所以考虑枚举:

① a,b,c中重合了多少数;

② a,b中重合了多少数;

③ a,c中重合了多少数;

④ b,c中重合了多少数;

⑤ a,b,c各出现了多少个数。

枚举完之后再乘个组合系数就行了。

 

504. 2019CCPC女生赛-String【贪心 + DP】

空间复杂度有点大?O(n2610), 其中n=105。【这道题的数据貌似有点问题,迷惑,我特判了ans=0的情况竟然wa了,不特判居然过了】

因为题目说至多k段,所以贪心地选长度为L来修改是合理的。定义状态:dp[i][c][j]表示做了前i位,最后一个字符是c,有j段的答案。转移方式为:

dp[i][c][j]={dp[iL][c][j]+1cstr[i]dp[iL][cc][j1]+1cstr[i]j>0dp[i1][c][j]c=str[i]dp[i1][cc][j1]c=str[i]j>0

查看代码
 const int maxn = 1e5 + 3, inf = 0x3f3f3f3f;
int n, l, k, dp[maxn][26][11], pre[maxn][26][11], suf[maxn][26][11];
char str[maxn];

int main() {
  ios_fast;
  cin >> n >> l >> k >> str + 1;
  memset(dp, 0x3f, sizeof(dp));
  for (int i = 0; i < 26; i++) {
    dp[1][i][1] = (i == str[1] - 'a');
  }
  for (int i = 2; i <= n; i++) {
    for (int j = 1; j <= i && j <= k; j++) {
      // prework
      pre[i - 1][0][j] = dp[i - 1][0][j];
      suf[i - 1][25][j] = dp[i - 1][25][j];
      for (int c = 1; c < 26; c++)
        pre[i - 1][c][j] = min(dp[i - 1][c][j], pre[i - 1][c - 1][j]);
      for (int c = 24; ~c; c--)
        suf[i - 1][c][j] = min(dp[i - 1][c][j], suf[i - 1][c + 1][j]);
      // trans
      int x = max(i - l, 0);
      for (int c = 0; c < 26; c++) {
        if (c == str[i] - 'a') {
          dp[i][c][j] = dp[i - 1][c][j];
          if (j > 1) {  // 特判无消耗转移
            int t0 = c ? pre[x][c - 1][j - 1] : inf;
            int t1 = c < 25 ? suf[x][c + 1][j - 1] : inf;
            dp[i][c][j] = min({dp[i][c][j], t0, t1});
          }
        } else {
          if (x) {
            dp[i][c][j] = dp[x][c][j] + 1;
            if (j > 1) {
              int t0 = c ? pre[x][c - 1][j - 1] : inf;
              int t1 = c < 25 ? suf[x][c + 1][j - 1] : inf;
              dp[i][c][j] = min(t0, t1) + 1;
            }
          } else {
            dp[i][c][1] = 1;
          }
        }
      }
    }
  }
  int ans = inf;
  for (int i = 0; i < 26; i++)
    for (int j = 0; j <= k; j++) ans = min(ans, dp[n][i][j]);
  cout << ans << endl;
}

 

505. 2022年SCUT-新生杯B题 - AC! 【区间DP / 反悔贪心】

题意:给你一个串S,只包含a、c、p三种字符,有两种操作,每次你可以选择一种操作来执行:

  • 你选择S中的子串 'ac' ,然后删掉它。
  • 你选择S中的子串 'cp', 然后删掉它。

每次删掉两个字符,串都会重新拼接起来。你的任务是将整个串删空,但是直接删可能无法将整个串删空,所以你可以在字符串S的任意一个地方插入一个任意的字符。现在问你最少插入多少个字符,使得整个串被删掉。原题n=300,但是实际上贪心可以O(n)通过。

(方法1:区间DP)

初始状态:dp[i][j]={0i>j1i=jinfi<j.

转移方程:有两种转移方式

  • 如果s[i]+s[j]==ac/cp 可以有 dp[i][j]=min(dp[i][j],dp[i+1][j1])
  • 然后都要进行区间DP的转移方式:dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]

(方法2:反悔贪心)

 

 

 

506. E.Master of Subgraph【点分治优化 + bitset表示状态的DP】【查看聚聚的题解

这道题一点头绪都没有。。。主要是连搜索都不会写,然后DP的状态根本想不出来。【这题数据范围n=3000,但是数据中边集好像出现了大于3000的点,开成6000就能过了】

考虑一个O(n2)的暴力,我们枚举每一个点作为连通块的必过点Root(连通块必须包含这个节点)。定义`bitset<100000> dp[x]`表示dfs到x节点(以及一部分x的孙子节点)的时候,包含Root和x的联通块的权值。【这个"dfs到x节点"的说法有点巧妙,之前都没见过这样的】

那么从x再dfs到某个儿子节点v的时候,dp[x]应该转移给dp[v],所以`dp[v] = dp[x] << w[v]`。如果v是一个叶子节点的话,dp[v]就直接是v节点的答案了。那么现在从v退栈到x,应该让`dp[x]|=dp[v]`。这样的话,就不需要做很多次背包/卷积了。因为在枚举x的下一个儿子节点`nxt_v`的时候,就会有`dp[nxt_v]=dp[x]<<w[nxt_v]`,就达到了背包的组合效果(x, 感觉很神奇。

现在复杂度是O(n210000064),实际上我们枚举了很多重复的状态(很多重复的连通块)。

所以我们考虑一个Root被选了之后,就把它的所有子树单独求解,这样就不会重复计算了。然后想到这里应该能想到点分治优化这个分治过程。最后复杂度就是\(O(\frac{nlogn*100000}{64})。

点分治代码
#include <bits/stdc++.h>
using namespace std;
#define ios_fast ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)

const int maxn = 3e3 + 11;

// 输入信息
int n, m, w[maxn], head[maxn], etop;
bitset<100005> dp[maxn], ans;
vector<vector<int>> e;

// 点分治信息
int root, mx_son_sz[maxn], sz[maxn], vis[maxn];

void addEdge(int x, int y) {
  e[++etop] = {y, head[x]}, head[x] = etop;
  e[++etop] = {x, head[y]}, head[y] = etop;
}

void find_root(int x, int p, int tot_sz) {  // totsz不可以加引用
  mx_son_sz[x] = 0, sz[x] = 1;
  for (const int & v : e[x]) {
    if (vis[v] || v == p) continue;
    find_root(v, x, tot_sz);
    sz[x] += sz[v];
    if (mx_son_sz[x] < sz[v]) mx_son_sz[x] = sz[v];
  }
  mx_son_sz[x] = max(mx_son_sz[x], tot_sz - sz[x]);
  if (root == -1 || mx_son_sz[x] < mx_son_sz[root]) root = x;
}

void dfs(int x, int p) {
  sz[x] = 1;  // 这里重新计算sz也行,不算也行,不算的话,只有父节点的sz是错误的,但是也不影响复杂度.[跳到line-51:①]
  dp[x] = dp[p] << w[x];
  for (const int & v : e[x]) {
    if (vis[v] || v == p) continue;
    dfs(v, x);
    sz[x] += sz[v];
    dp[x] |= dp[v];
  }
}

void Work(int x, int tot_sz) {
  root = -1;
  find_root(x, 0, tot_sz);
  vis[root] = true;
  dfs(root, 0);
  ans |= dp[root];
  x = root;  // 注意赋值记录root
  for (const int & v : e[x]) {
    if (!vis[v]) Work(v, sz[v]); // 这里①:如果上面不算,只有x的父节点的sz是错的
  }
}

void solve() {
  cin >> n >> m;
  e.assign(2 * n + 1, vector<int>());
  for (int i = 1, x, y; i < n; i++) {
    cin >> x >> y;
    e[x].push_back(y);
    e[y].push_back(x);
  }
  for (int i = 1; i <= n; i++) {
    cin >> w[i];
    dp[i].reset();
    vis[i] = false;
  }
  dp[0].reset();
  ans.reset();
  dp[0][0] = 1;  // 初始化特例
  Work(1, n);
  assert(m <= 100000);
  for (int i = 1; i <= m; i++) cout << ans[i];
  cout << "\n";
}

int main() {
  ios_fast;
  int T;
  cin >> T;
  while (T--) solve();
}

 

507. I. Incoming Asteroids【经典:三个点的权值之和大于x + 势能思想】【提交记录

题意大致如下:有n个位置可以假设照相机,然后有m个时间发生。

  1. 事件1:有一个人在q[1..k]这k个位置各放了1个照相机,只有当照相机拍满y分钟的视频之后才会满足条件。满足条件之后就不架了。
  2. 事件2:在位置x出现了长达y分钟的事件,在位置x架设的照相机可以拍到y分钟的视频。

每个事件2之后输出满足条件的事件1的ID。要求强制在线。

如果不要求强制在线,可以离线用二分来做,也是O(nlognlognk)

如果一个事件1要拍y分钟,那么一定存在一个位置拍了yk分钟及以上的。我们把这个时间戳放进一个堆里面,当一个位置x加了y的时候,检查一下x节点的堆是否有事件1的时间戳被满足。

这样不停减13直到变成0。所以整体复杂度为nlognlog23n

 

508. 2019香港-E. Erasing Numbers【贪心 + 思维 + 结论】

题意:给你n=2000的一个序列(保证任意两个数互不相同),每次可以选择连续的3个数,然后保留其中的中位数,删掉剩下两个数。请你判断是否能通过一系列操作最后只剩第i个数。

重点:最重要的就是怎么实现贪心的过程了。

① 假设现在是0比较多,即delta<0。现在应该让0元素之间内耗,从而达到1的数量和0的数量相等。而0内耗的唯一方法就是30连在一起,即cnt0=3的时候发生。此时发生内耗,cnt02变成1完成内耗。基于这个思想,我们进行下面的分类讨论:

  • 遇到的是0
    • 直接cnt0++即可。
  • 遇到的是1:这个不是我们希望遇到的,因为这会导致我们无法消掉连续的0
    • 此时cnt0=0,说明左边的0已经内耗完成,直接抛弃这个1就行了。
    • 此时cnt0>0,说明左边还有0存在,可以等待和右边的0内耗,所以可以cnt0=1消掉这个1尽可能把0传到右边去。
  • 遇到的是a[pos]
    • 0肯定无法越过pos,所以直接赋值cnt0=0即可。

② 如果是1比较多,同样分析就行。

查看代码
 const int maxn = 5e3 + 1;

int n, a[maxn];

int test(int pos) {
	int delta = 0, cnt = 0, tot = 0;
	for (int i = 1; i <= n; i++) 
		if (i != pos) delta += a[i] < a[pos] ? -1 : 1;
	for (int i = 1; i <= n; i++) {
		if (pos == i) {
			cnt = 0;
		} else if (a[i] < a[pos]) {
			cnt--;
			if (delta < 0) {
				if (cnt == -3) cnt += 2, tot++;
			} else {
				cnt = max(cnt, 0);  // -1不是多的,如果不取max的话,后续会消耗1而导致tot++的机会变少,所以直接抛弃
			}
		} else if (a[i] > a[pos]) {
			cnt++;	
			if (delta > 0) {
				if (cnt == 3) cnt -= 2, tot++;
			} else {
				cnt = min(cnt, 0);  // 1不要消耗-1,所以直接抛弃这个1
			}
		}
	}
	return 2 * tot >= abs(delta);
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while (T--) {
		cin >> n;
		for (int i = 1; i <= n; i++) cin >> a[i];
		for (int i = 1; i <= n; i++) cout << test(i);
		cout << "\n";
	}
}

 

 

509. H. Harmonious Rectangle【抽屉原理】

题意:给定一个nm的网格,每个位置可以涂上颜色1、2、3这3种颜色。求有多少个合法的涂色方案使得存在四元组(x1,y1,x2,y2)满足x1<x2y1<y2,以及下面两个条件之一:

  1. color(x1,y1)=color(x1,y2)color(x2,y1)=color(x2,y2)
  2. color(x1,y1)=color(x2,y1)color(x1,y2)=color(x2,y2)

思路:

考虑一个二元组(x,y)33种涂色方案,在102的网格里,必然存在两行是满足上面条件的。然后dfs暴力搜出99以内的所有不合法的解。对于n>9或者m>9的输入,直接输出3nm就行了。

 

510. E. Identical Parity【exgcd + 思维】

思路:

AC=n%k,BC=kn%k,那么有AC个块的长度是A=nk, BC个块的长度是B=nk

同时1的数量one=n2, 0的数量zero=n2

设把x个大块设置为1,y个小块设置为1,那么有:xA+yB=one 

同时还有:(ACx)A+(BCy)B=zero

所以得到了这些限制条件:

one=ACA+BCBzeron=ACA+BCB - 这个条件一开始check就行了

x[0,AC]y[0,BC] 

我们先求出方程xA+yB=one 的特解x0,y0, 那么有:x0+Bgcdt[0,AC]y0Agcdt[0,BC]

得到t的两个合法范围区间:t[lx,rx]t[ly,ry]。最后检查max(lx,ly)min(rx,ry)是否成立就行了。

查看代码
 long long n, k;
 
long long exgcd(long long a, long long b, long long &x, long long &y) {
  if (!b) {
    x = 1, y = 0;
    return a;
  } else {
    long long g = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return g;
  }
}
 
void solve() {
  cin >> n >> k;
  long long one = (n + 1) / 2;
  long long even = n - one;
  long long a_cnt = n % k, a = (n + k - 1) / k;
  long long b_cnt = k - a_cnt, b = n / k;
  if (n != a_cnt * a + b_cnt * b) {  // 特判不合法情况
    cout << "No\n";
    return;
  }
  long long x, y, A = a, B = b, C = one, g = exgcd(A, B, x, y);
  if (C % g || !A || !B) {  // 特判无解
    cout << "No\n";
    return;
  }
  C /= g, A /= g, B /= g;
  x *= C, y *= C;  // 记得通解乘上C
  long long Lx = (int)ceil((-1.0 * x) / (1.0 * B)), Rx = (int)floor((1.0 * a_cnt - x) / (1.0 * B));
  long long Ry = (int)floor((-1.0 * y) / (-1.0 * A)), Ly = (int)ceil((1.0 * b_cnt - y) / (-1.0 * A));  // 乘一个负数,区间翻转
  if (max(Lx, Ly) <= min(Rx, Ry)) {
    cout << "Yes\n";
  } else {
    cout << "No\n";
  }
}

 

511. F. Hossam and Range Minimum Query【主席树 + 随机集合哈希】

题意:1e5次询问,对于一个长度为1e5的序列,求区间内出现次数为奇数的最小值,强制在线。

看到强制在线,要么考虑分块,要么考虑主席树等等。

分块我一开始想了想,发现需要用到值域分块,而且值域分块合并不了信息,所以不能做。

考虑主席树(多个版本的权值线段树),区间内怎么判断是不是出现次数为奇数呢?

我们考虑去掉所有出现次数为偶数的数。即建立主席树的时候,遇到一个数是第"奇数次"出现的,那么就+rand_hash_value,否则就-rand_hash_value。也可以使用异或+nealhash。

最后用L-1的主席树和R的主席树去查询,如果左节点的hash值相同(这里涉及到pushup函数合并hash值,具体你自己设计就行,只要不发生hash冲突),那说明左侧没有出现过奇数次的数,否则就说明左侧出现过。(这个就是主席树上二分的过程)

查看代码
 struct custom_hash {
  static uint64_t splitmix64(uint64_t x) {
    x += 0x9e3779b97f4a7c15;
    x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
    x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
    return x ^ (x >> 31);
  }
  size_t operator()(uint64_t x) const {
    static const uint64_t FIXED_RANDOM =
        chrono::steady_clock::now().time_since_epoch().count();
    return splitmix64(x + FIXED_RANDOM);
  }
} Hash;

int n, q, l, r, a[maxn], s[maxn], sc;
int top, rt[maxn], ls[maxn * 20], rs[maxn * 20];
uint64_t H[maxn * 20];
#define mseg ((l + r) >> 1)

int id(int x) { return lower_bound(s + 1, s + 1 + sc, x) - s; }

int insert(int p, int l, int r, int x) {
  int ro = ++top;
  ls[ro] = ls[p], rs[ro] = rs[p], H[ro] = Hash(s[x]) ^ H[p];
  if (l == r) return ro;
  if (x <= mseg) ls[ro] = insert(ls[p], l, mseg, x);
  else rs[ro] = insert(rs[p], mseg + 1, r, x);
  return ro;
}

int query(int a, int b, int l, int r) {
  if (l > r) return 0;
  if (l == r) {
    return H[a] == H[b] ? 0 : s[l];
  } else if (H[ls[a]] == H[ls[b]]) {
    return query(rs[a], rs[b], mseg + 1, r);
  } else {
    return query(ls[a], ls[b], l, mseg);
  }
}

signed main() {
  ios::sync_with_stdio();
  cin.tie(0), cout.tie(0);
  cin >> n;
  for (int i = 1; i <= n; i++) cin >> a[i], s[i] = a[i];
  sort(s + 1, s + 1 + n);
  sc = unique(s + 1, s + 1 + n) - s - 1;
  for (int i = 1; i <= n; i++) 
    rt[i] = insert(rt[i - 1], 1, sc, id(a[i]));
  cin >> q;
  int lasans = 0;
  while (q--) {
    cin >> l >> r;
    l ^= lasans;
    r ^= lasans;
    lasans = query(rt[l - 1], rt[r], 1, sc);
    cout << lasans << "\n";
  }
}

 

512. B - Arrange Your Balls【构造】

题意:定义cnt为(x,y)的对数,其中xy相邻且(x,y)。且现在给你一个环,让你重排一些位置,使得cnt最少。

我们把两种颜色相邻看成一条边,那么这显然是一个无向图。所以答案最小只能是k-1,其中k是颜色的数量。最小的情况下,颜色作为节点形成一棵树。也就是说,答案要么是k-1,要么是k。

我们现在判断能不能形成一棵树。首先把所有颜色的出现次数设置为min(count, k-1),因为只有k-1是有效的!

然后每次取出一个出现次数最小的节点,把他当成叶子节点,挂在另一个非叶子节点上。我们贪心地选择出现次数最大地节点为这个非叶子节点。如果两个都是叶子节点,这种情况只能出现在最后一刻,否则说明无解,答案只能是k。我们得到树形结构之后,使用欧拉序,相邻的颜色同样只有k-1对,就能构造出k-1的答案。很神奇。(性质:欧拉序上相邻的位置在树上也是相邻的)

对了,杜老师是基于一个性质提出这个构造方法的:颜色出现的次数必须大于等于节点的度数。

【扩展】给你一个度数序列,构造出一个简单图。

(和上面恰恰相反)每一次找度数最大的点,把它的度数分配到后面的点(从大往小排序)。直到度数不够后面的点分(比如:3 3 1 1)就说明答案无解,否则就构造出一个贪心的解。

 

513. E - GCD of Path Weights【图论 + GCD转换为取模 + 势能思想】【提交记录

题意:给定一个有向无环图(因为u->v一定有u<v,所以无环),同时给你一个点权序列A,(a[i]=-1说明点权未确定)求1到n的所有路径权值(路径权值为点权之和)的gcd。

(1)总结套路:

杜老师说遇到这类全部路径的题,可以考虑定义一个函数Pi代表1到i所有路径的一个权值,至于这个权值的含义由题目解法决定。

但是这个题实际上先转换问题才能使用这个套路。

(2)考虑已经知道答案,假设答案为X

现在定义Pi为从1到i的所有路径的权值之和模X得到的余数。那么有:

  • P1=PnmodX
  • Pu+Wu=PvmodXPu+WuPv=0modX 
    • 我们在后面的讨论中都不再考虑Au=1的情况,因为一个未知数导致该等式恒成立,也就是Au不会影响到X的大小。这也就是为什么我们后面不对它连边。

这就是我们通过假设得到的全部信息。我们也考虑通过这些信息构造出一个最大的答案X

(3)如何根据这个模型贪心构造捏?

因为PnP1=0modXPu+WuPv=0modX成立,所以X必然是PnP1Pu+WuPv的一个因子。所以X=gcd(Pu+WuPv,.....)即对所有的边取一遍GCD。最后再X=gcd(PnP1,X)

不仅仅如此,我们有办法让取gcd的次数尽可能少。比如 Pv=Pu+Wu的过程,加法对任意的模数都是成立的,也就是说,当Pv是从Pu推导过来的时候,是不影响答案的。所以我们应该建一个无向图(而不是原题的有向无环图),对于同一个连通块,我们应该尽可能地从1个点推导出所有地点(因为取gcd的次数肯定越少越好)。对于无向图的一个环,我们没有办法,只能通过gcd来合并这个答案了。

 

【Winter-Camp构造】

(Camp构造小专题)增量构造 

  • 定义?:根据n-1或者n-2的答案构造出n的答案,或者根据n的答案构造出n-1的答案。

514:E. Oscar is All You Need【构造 + 增量构造例题】

没写出来,太烦了不写了。

 

(Camp构造小专题)调增法

515:Graph Coloring

题意:给你一个n=1e5的无向简单图,让你对图进行三染色,同时保证每个节点的度数不超过5,要满足颜色和自身相同的点不能超过1个。输出最后的染色方案,保证有解。

题解:大概就是暴力地check和修改,每次修改完之后还要check邻边的点,这样并不能保证每个点只会被遍历一遍,但是为了避免一个点在对列中多次出现,还是要维护一个vis数组。总的复杂度比较玄学和随机,但是毕竟图限制得很死,所以应该是能过的。

查看代码
 #include <bits/stdc++.h>
using namespace std;

const int maxn = 1e5 + 11;
int n, m, c[maxn], vis[maxn];
vector<vector<int>> e;

bool check(int x) {
  int cnt = 0;
  for (int v : e[x])
    if (c[v] == c[x]) cnt++;
  return cnt > 1;
}

int Work(int x) {
  int cnt[3] = {0, 0, 0};
  for (const int& v : e[x]) cnt[c[v]]++;
  int mn = min({cnt[0], cnt[1], cnt[2]});
  if (mn == cnt[0]) return c[x] = 0;
  if (mn == cnt[1]) return c[x] = 1;
  return c[x] = 2;
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  double st = clock();
  freopen("test_input.txt", "r", stdin);
  freopen("test_output.txt", "w", stdout);
  cerr << "start" << endl;
  cin >> n >> m;
  e.assign(n + 1, {});
  size_t mx_deg = 0;
  for (int i = 1, x, y; i <= m; i++) {
    cin >> x >> y;
    e[x].push_back(y);
    e[y].push_back(x);
    mx_deg = max(mx_deg, e[x].size());
    mx_deg = max(mx_deg, e[y].size());
  }
  cerr << "input ok - max_deg = " << mx_deg << endl;
  for (int i = 1; i <= n; i++) c[i] = i % 3;
  queue<int> q;
  for (int i = 1; i <= n; i++)
    if (check(i)) q.push(i), vis[i] = 1;
  while (q.size()) {
    int x = q.front();
    Work(x), q.pop(), vis[x] = 0;
    for (const int& v : e[x])
      if (!vis[v] && check(v)) q.push(v);
  }
  cerr << "solve ok" << endl;
  for (int i = 1; i <= n; i++) cout << c[i] << " ";
  cout << endl;
  double et = clock();
  cerr << "OK! " << (et - st) << "ms" << endl;
}

 

516:[ARC076F] Exhausted 【霍尔定理 + 二分图匹配 或者 反悔贪心】

(1)方法1:霍尔定理

霍尔定理:对于一个二分图{X,Y},任意一个X的子集X都满足:|X||Nb(X)|是该二分图能完美匹配的充要条件。

推论-最大匹配MaxMatch=|X|max(0,|X||Nb(X)|)。其中X取遍X的子集,Nb(X)X的相邻节点的集合。 

一看题意,就好像能用二分图匹配来做。但是直接建图会TLE。我们知道答案就是nMaxMatch。考虑到图的特殊性,使用霍尔定理来计算最大匹配。把人作为X部点,那么Nb(X)=max{Lx}+(mmin{Rx}+1)。但是我们不可能真的枚举所有子集。所以我们枚举maxL【使用扫描线的思想】,然后把所有LimaxL的人都放入到某个数据结构中,再从这些人里面选一个集合出来,使得|X|(mmin{Rx}+1)maxL最大。使用线段树维护后缀最大值就可以做到。

(2)方法2:反悔贪心【题解

我们把人按L从小到大排序,然后正着贪心一遍,把人尽可能往左边坐。那么剩下了一些人,我们让这些人的Ri尽可能小。具体怎么做呢?

一开始设置数组suf为全-1,然后如果坐了一个人就是0,否则就是证明有suf[i]个人的坐标要大于等于i。

对于当前枚举到第i个人,左边已经坐到curL

  1. Li>curL: 这个人可以继续往左边坐。
  2. Li=curL: 从所有L<curL的人中,找到一个R最小的,让他尽可能往右边坐,即只能坐[R,...]区间。

这么贪心到最后,我们再倒着贪心,做一遍后缀和就是贪心的结果。

 

 

517:Pairs of Pairs【无向图DFS树 + 构造思想】

呃,这个题的最重要的思想就是:

  1. 如果树的深度没有n2,那么说明每一层就算少一个,最后的节点数量一定至少有n2个。
  2. 我们按层来划分节点对,一定不会多余两条边:
    1.  因为同一层之间不存在边,那么只剩下返祖边和儿子边。
    2.  因为图不存在重边,所以要么是返祖边(返祖边返回的一定是祖先),要么是儿子边。
    3.  然后对于返祖边,一层只能有一个祖先,所以最多只能有2条边。如果是儿子和父亲之间的边,也最多只有2条。

至此,构造的正确性就全部说明了。

 

518:C. Johnny Solving【无向图DFS树 + 度数至少为3 + 构造思想】

如果DFS树的深度超过nk,那么就是任务1。

否则树的叶子节点一定超过k个。 题目说节点度数大于等于3,所以构造一下就是答案了。

 

519:Hidden Supervisors 【树上贪心 + 排序细节】

结论:树上最大匹配可以贪心地做。

然后排序也很重要,应该先让空余节点多的子树排前面。

 

520:Problem H. 还原神作 【WQS二分优化DP】【WQS二分教程

对于最小值,显然具有一个暴力DP的解法。我们可以考虑 wqs优化。

二分斜率M,感性理解就是:我们省略掉代价大于M的转移,那么二分出来的就是最低转移代价(也就是说,在这道题目中,斜率就是转移代价)。

注意这个重点!!!!

  • DP转移应该先使得答案最优,再从最优答案里面,使得转移的段数K最大。而不是先让K最大,然后再让答案最优。(我因为这里wa3)

其实对于wqs二分的题目,我一直没有考虑过严格证明,也不会严格证明,只是通过感性理解觉得能够二分这个代价,然后套上模板。

下面是看完教程之后的一个感性的证明。

假设g(i)K=i时的答案,显然他是一个单调不减的函数,甚至是斜率单调不减(这个很重要,这意味着这个函数是一个下凸包)。因为你多选一对,这一对的代价一定不小于之前选择的,否则在之前选择会使得答案更小。(以下小写k代表斜率,大写K代表题目给定的要求)

然后我们考虑使用一条直线去切这个函数,固定一个斜率k,直线y=kx+b切这个下凸包,得到的b一定比其它b更小(这和我们最小化答案g(i)的方向是一致的) - 这也解释了上面为什么要先最小化答案,再最大化段数,否则你就不是在求切点。我们先考虑求出在x=K处的切线,即先求出b,那么最终答案就是g(K)=b+kK - 这是因为直线的斜率是k,切点位于x轴x=K处,截距加上kK就是切点的纵坐标。

我们怎么求这个b呢?因为斜率k越大,切点就越往右走,所以我们考虑二分出这个斜率k,然后b=ykx可以看出,每一步被记录的转移,代价减少了k。而我们最后求出的DP数组f和计数数组c,就是在斜率k下,得到的截距b和横坐标x

直到找到了切点横坐标x=K的那条切线,我们就找到了答案。

 

 

521:G - Go Territory (atcoder.jp)

枚举X轴,然后用Y相邻的两个点围出来的线段当成一个点。然后从X=1到X=2e5枚举一遍,如果相邻的线段有重叠,那么两个点之间连一条无向边。这样连完之后,跑一边BFS,看看从0号线段能不能到达就行了。如果不能到达,就意味着被封了起来。对这些线段求和就好了。

 

posted @   PigeonG  阅读(114)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示