NOIP模拟54
这套题非常NOIP
T1:
首先复盘,考场上首先先观察信息发现每个点度数最多为10,尽管并未学习边分治
然而这种边数条件限制的确可以保证边分治的复杂度(这也使得我并未想到状压DP)
接着考虑点分治(大量处理路径问题),然而发现并不会处理路径重复问题
然后则是树形DP,比较容易想到的是考虑转化为每条边的贡献,关键限制条件为每条边只能经过一次,那么很显然的最大覆盖问题模型,
然后就无了(再次显示我DP程度之差,仍然为想到用状压DP解决最大覆盖问题)(事实上状压专题中愤怒的小鸟就是经典的覆盖问题,貌似可以通过Dancing Lines解决)
接下来是正解部分,考虑每条边的贡献后,利用树的分治属性,考虑是否可以将问题转移到子树上并进行合并,
那么很直接的思路为计算每棵子树的最大路径数合并到根并处理合并部分,证明是否正确:
若每个子树都计算路径最大值,考虑那么对于当前根节点合并造成的影响只有根节点与子节点之间的连边所造成的路径,
那么分类讨论,当根节点与子节点间的连边与所有子树内路径并不冲突时显然可以直接累加,
当冲突数为1时相当于用一条路径更换另一条路径,然而仔细考虑发现,对于子树中的路径显然比经过根节点的路径覆盖深度小,
由于每条边只能使一条路径通过,那么选择覆盖深度小的路径显然会为接下来的选择增加更大的可行性,因此不替换显然更优,而当冲突数为2时同样显然
那么可以得到合并时的策略,即当且仅当存在一条路径经过根节点且不与之前路径冲突时累加答案
那么现在的问题即为判断是否存在路径经过根节点,当子树内路径选择完毕后,当且仅当存在点能够不产生冲突的到达根节点时才可能会产生有贡献的路径,
那么我们可以使用vector记录子树中所有可以不产生冲突到达根节点的点(然而考虑复杂度比较直接的思路为直接记录一定能经过x的点而非记录能够到达其根节点的点,
这里如此维护是因为在之后的计算中也可能用到这些点,而若直接针对x进行维护,当根节点更换时重新维护的代价极大,因此一定情况下可以舍弃一定的复杂度,
通过维护灵活性较强容易转换的信息来应对处理对象不断变化的情况)
对于最大覆盖问题,状压DP非常套路的设f[s]为考虑选择的子节点集合为s,转移时状态划分即可
考虑如何维护vector,发现对于当前根节点的最大路径数,所有能够到达最大路径数的状态中未被选择到的子节点(说明根节点到该子节点间的路径未被选择)的vector都能够到达根节点
于是在状压DP过程中记录所有能到达最大路径数的状态,最后统一合并可行子节点的vector即可
代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 #define I int 4 #define C char 5 #define B bool 6 #define V void 7 #define LL long long 8 #define P pair<I,I> 9 #define MP make_pair 10 #define fir first 11 #define sec second 12 const I N = 1e3 + 3; 13 const I M = 1e5 + 3; 14 I n,m,ans; 15 I tot,head[N],to[M << 1],nxt[M << 1]; 16 B jud[N][N]; 17 vector <I> connect[N]; 18 inline I read() { 19 I x(0),y(1); C z(getchar()); 20 while (!isdigit(z)) { if (z == '-') y = -1; z = getchar(); } 21 while ( isdigit(z)) x = x * 10 + (z ^ 48), z = getchar(); 22 return x * y; 23 } 24 inline V found (I x,I y) { 25 to[++tot] = y, nxt[tot] = head[x], head[x] = tot; 26 to[++tot] = x, nxt[tot] = head[y], head[y] = tot; 27 } 28 inline I max (I a,I b) { return a > b ? a : b; } 29 inline I min (I a,I b) { return a < b ? a : b; } 30 inline V swap (I &a,I &b) { a ^= b, b ^= a, a ^= b; } 31 V Dfs (I x,I father) { 32 I cnt(0),rec[10],f[1 << 10],MA(INT_MIN); B link[10][10],typ[10]; 33 vector <I> tmp; 34 memset (rec,0,sizeof rec); memset (f,0,sizeof f); 35 memset (link,0,sizeof link); memset (typ,0,sizeof typ); 36 for (I i(head[x]),y(to[i]); i ;i = nxt[i],y = to[i]) if (y != father) 37 rec[cnt++] = y, Dfs (y,x); 38 for (I i(0);i < cnt; ++ i) { 39 if (!connect[rec[i]].size ()) continue; 40 for (I j(i + 1);j < cnt; ++ j) { 41 if (!connect[rec[j]].size ()) continue; 42 for (auto p1 : connect[rec[i]]) { 43 for (auto p2 : connect[rec[j]]) 44 if (jud[p1][p2]) { 45 link[i][j] = link[j][i] = 1; 46 f[1 << i | 1 << j] = 1; break; 47 } 48 if (link[i][j]) break; 49 } 50 } 51 } 52 for (I i(0);i < cnt; ++ i) { 53 if (!connect[rec[i]].size ()) continue; 54 for (auto p1 : connect[rec[i]]) 55 if (jud[x][p1]) { link[i][i] = 1; f[1 << i] = 1; break; } 56 } 57 for (I i(0);i < 1 << cnt; ++ i) { 58 for (I j(0);j < cnt; ++ j) { 59 if (!(i & 1 << j)) continue; 60 if (link[j][j]) 61 f[i] = max (f[i],f[i ^ 1 << j] + 1); 62 for (I k(j + 1);k < cnt; ++ k) { 63 if (!(i & 1 << k) || !link[k][j]) continue; 64 f[i] = max (f[i],f[i ^ 1 << j ^ 1 << k] + 1); 65 } 66 } 67 if (f[i] == MA) tmp.push_back (i); 68 if (f[i] > MA) { MA = f[i]; 69 tmp.clear (), tmp.push_back (i); 70 } 71 } 72 connect[x].push_back (x); 73 for (auto p1 : tmp) { 74 for (I i(0);i < cnt; ++ i) { 75 if (p1 & 1 << i || typ[i]) continue; 76 typ[i] = 1; 77 for (auto p2 : connect[rec[i]]) 78 connect[x].push_back (p2); 79 } 80 } 81 if (MA != INT_MIN) ans += MA; 82 } 83 signed main () { 84 n = read(); 85 for (I i(1);i <= n - 1; ++ i) found (read(),read()); 86 m = read(); 87 for (I i(1);i <= m; ++ i) { 88 I x(read()),y(read()); 89 jud[x][y] = jud[y][x] = 1; 90 } 91 Dfs (1,0); 92 printf ("%d\n",ans); 93 }
T4:
考场分析如何使得击杀顺序更优,于是设数带入发现与血量以及攻击力都呈相关性,然而由于代数的原因使得结论并不明显
教训是事实上数学式比代数直观,图形比数学式直观
考虑要求击杀顺序,那么化归问题,我们只需要比较两个怪的击杀顺序即可得到所有怪的优先级计算策略
那么设ci = (di - 1) / b + 1为i的击杀所需次数,考虑对于i,j中i比j先击杀更优当且仅当:
ai * (ci - 1) + aj * ci + aj * (cj - 1) > aj * (cj - 1) + ai * cj + ai * (ci - 1)(考虑击杀两只怪物不同顺序的承受伤害比较即可)
得到:aj * ci > ai * cj 那么按斜率依次排序即可
考虑秒杀所造成的影响,设ei为秒杀i所减少的伤害数,则:
ei = ai * sigma(cj) + ai * (ci - 1) + sigma(aj) * ci
直接思路为n^2枚举ei,ej,统计最大值,考虑如何优化
比较套路的是钦定选i,考虑秒杀j比秒杀k更优的情况,即:
ei + ej - cj * ai > ei + ek - ck * ai(需要减去重复计算的部分)
显然可以转化为斜率式:ai < (ej - ek) / (cj - ck)
考虑首先转化的前提为cj > ck因此需要对c排序,又由于是<
因此当满足条件时ai只会选择更优的点,因此需要维护左上凸包
斜率优化的注意点一为提取常量,二为斜率同趋势
因此又需要将a从大到小排序,于是考虑CDQ分治
首先二分区间,在区间左侧按c排序,在区间右侧按a排序,保证趋势相同
然后更新单调栈最后将右侧与a进行匹配即可
需要注意的是由于需要保证按斜率排序,于是在处理当前区间时首先要分治左右区间
再处理当前区间(事实上再排序一遍也可),CDQ的主要思想极为利用大趋势的相同
来分别处理不同部分
代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 #define I long long 4 #define C char 5 #define B bool 6 #define V void 7 #define LL long long 8 #define P pair<I,I> 9 #define MP make_pair 10 #define fir first 11 #define sec second 12 const I N = 3e5 + 3; 13 I n,b,sum,ans,MA,pre1[N],pre2[N],q[N]; 14 struct L {I a,c,e;} a[N]; 15 inline I read() { 16 I x(0),y(1); C z(getchar()); 17 while (!isdigit(z)) { if (z == '-') y = -1; z = getchar(); } 18 while ( isdigit(z)) x = x * 10 + (z ^ 48), z = getchar(); 19 return x * y; 20 } 21 inline I max (I a,I b) { return a > b ? a : b; } 22 inline I min (I a,I b) { return a < b ? a : b; } 23 inline V swap (I &a,I &b) { a ^= b, b ^= a, a ^= b; } 24 inline V Max (I &a,I b) { a = a > b ? a : b; } 25 inline V Min (I &a,I b) { a = a < b ? a : b; } 26 inline B com1 (const L &a,const L &b) { return a.c * b.a < b.c * a.a; } 27 inline B com2 (const L &a,const L &b) { return a.c < b.c; } 28 inline B com3 (const L &a,const L &b) { return a.a > b.a; } 29 inline B Check1 (I i,I j,I k) { //斜率相同 30 return (a[i].e - a[j].e) * (a[j].c - a[k].c) >= (a[j].e - a[k].e) * (a[i].c - a[j].c); 31 } 32 inline B Check2 (I k,I i,I j) { 33 return a[k].a * (a[i].c - a[j].c) < a[i].e - a[j].e; 34 } 35 V Divide (I l,I r) { 36 if (l == r) return ; 37 I mid (l + r >> 1),h(1),t(0); 38 Divide (l,mid), Divide (mid + 1,r); 39 sort (a + l,a + mid + 1,com2); 40 sort (a + mid + 1,a + r + 1,com3); 41 for (I i(l);i <= mid; ++ i) { 42 while (h <= t && Check1 (i,q[t],q[t - 1])) t -- ; q[++t] = i; 43 } 44 for (I i(mid + 1);i <= r; ++ i) { 45 while (h < t && Check2 (i,q[h + 1],q[h])) h ++ ; 46 Max (MA,a[i].e + a[q[h]].e - a[q[h]].c * a[i].a); 47 } 48 } 49 signed main () { 50 freopen ("fittest.in","r",stdin); 51 freopen ("fittest.out","w",stdout); 52 n = read(), b = read(); 53 for (I i(1);i <= n; ++ i) { 54 I x(read()),y(read()); sum += x; 55 a[i] = (L) {x,(y - 1) / b + 1,0}; 56 } 57 sort (a + 1,a + n + 1,com1); 58 for (I i(1);i <= n; ++ i) 59 pre1[i] = pre1[i - 1] + a[i].c; 60 for (I i(n);i >= 1; -- i) 61 pre2[i] = pre2[i + 1] + a[i].a; 62 for (I i(1);i <= n; ++ i) 63 a[i].e = a[i].a * pre1[i - 1] + a[i].a * (a[i].c - 1) + a[i].c * pre2[i + 1]; 64 for (I i(1);i <= n; ++ i) 65 sum -= a[i].a, ans += a[i].a * (a[i].c - 1) + sum * a[i].c; 66 Divide (1,n); 67 printf ("%lld\n",ans - MA); 68 }
Update:维护凸包时当斜率相等时一般情况下也要出队,因为显然会取更优的点(当特别说明斜率相同时也可能需要作出选择)