NOIP模拟55
T1:
与模拟54T4较为相像,然而需要注意的点有所不同
首先比较显然的是DP方程:fi = fj + ai - (i - j) * (i - j - 1) / 2
常规思路为直接使用权值数据结构进行维护,然而无法处理后面的部分
考场上想过分块线段树树状数组,然而都失败了,于是突然发现对于 一点i
若i的可选部分中j为max,那么j之后的部分一定不会在成为max,于是考虑采用链表
通过截断链表的方式排除较劣值(仍然未想到斜率优化单调栈),然而发现问题一在于如何寻找前驱后继
我采用multiset维护,然而问题二在于前驱后继在本题中并不唯一,甚至可能形成环状结构,于是思路结束
通过昨天T1,我考虑能否降低维护的准确度以此换来信息的灵活性,然而仍然无法维护
事实上正解的思路并不是单纯的数据结构维护信息,而是决策单调性
类似模拟54T4,考虑在计算fi时j比k更优的情况(仍然套路的直接解不等式)化简得到:
设gi = fi - i * (i - 1) / 2
i > -(gj - gk) / (j - k)(i为常量,其余为变量)
根据>可以得出应维护右下凸包,同时递增
于是类似昨天T4的思路采取CDQ分治,利用左侧更新右侧
考虑首先消去a这一维,直接按a排序即可,接着在分治过程中按下标排序即可
注意点:首先模拟54未说的
单调队列的维护过程中,需要保证head >= tail,因为当队列中之存在一个点时,实际上还存在这原点,
这两个点的存在是的仍然能继续更新答案(可能需要特判横坐标相同的情况,或者在排序时修改排序函数)
而在单调队列获取答案的时候,则需要保证head > tail,因为当队列中仅存在一个点时,当取两点进行斜率比较时
将会取到head + 1,进行比较,而显然head + 1为队外元素(即非法),因此仅当队列中至少存在两个元素时才能弹队
其次是CDQ分治的顺序
模拟54T4中是先分治两侧,目的是保证有序,而本题需要先分治左侧,处理完当前区间后再处理右侧
原因在于模拟54T4的转移是不需要顺序的,只需要将前缀中所有元素进行转移即可
而对于本题而言,转移是存在顺序的,即必须从左往右转移,因为每个点需要继承之前点的贡献
(所以由于处理完当前区间后排序已经被打乱,需要重新对右区间进行排序)
另外还有一个不同点在于维护顺序
模拟54T4是先维护完单调栈再从右侧区间逐一进行弹栈取答案,原因在于其取答案并没有限制,即对于右侧区间所有点,取答案的条件是统一的
而对于本题而言,需要一边维护单调队列,一边取答案,因为显然,对于右侧区间内某点i,取答案的一个条件为id(j) < id(i),也就是说右侧区间取答案的条件不统一
可以发现,若先维护队列再取答案,由于弹队列不存在id限制,那么可能对于i点的可行答案会被弹走,因此会漏情况
代码如下:
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 #define g(x) (f[x] - x * (x + 1) / 2) 13 const I N = 1e5 + 3; 14 I n,q[N],ans(LONG_LONG_MIN),f[N]; 15 struct L {I a,b;} a[N]; 16 inline I read() { 17 I x(0),y(1); C z(getchar()); 18 while (!isdigit(z)) { if (z == '-') y = -1; z = getchar(); } 19 while ( isdigit(z)) x = x * 10 + (z ^ 48), z = getchar(); 20 return x * y; 21 } 22 inline V Max (I &a,I b) { a = a > b ? a : b; } 23 inline V Min (I &a,I b) { a = a < b ? a : b; } 24 inline I max (I a,I b) { return a > b ? a : b; } 25 inline I min (I a,I b) { return a < b ? a : b; } 26 inline V swap (I &a,I &b) { a ^= b, b ^= a, a ^= b; } 27 inline B com1 (const L &a,const L &b) { 28 return a.a == b.a ? a.b < b.b : a.a < b.a; 29 } 30 inline B com2 (const L &a,const L &b) { return a.b < b.b; } 31 inline B Check1 (I i,I j,I k) { 32 return (g(i) - g(j)) * (j - k) >= (g(j) - g(k)) * (i - j); 33 } 34 inline B Check2 (I k,I i,I j) { 35 return k * (i - j) >= g(j) - g(i); 36 } 37 V Divide (I l,I r) { 38 if (l == r) return ; 39 I mid (l + r >> 1),h(1),t(0),p1(l); 40 Divide (l,mid); 41 sort (a + l,a + mid + 1,com2); 42 sort (a + mid + 1,a + r + 1,com2); 43 for (I i(mid + 1);i <= r; ++ i) { 44 while (p1 <= mid && a[p1].b < a[i].b) { 45 while (h <= t && Check1 (a[p1].b,a[q[t]].b,a[q[t - 1]].b)) t -- ; q[++t] = p1 ++ ; 46 } 47 while (h < t && Check2 (a[i].b,a[q[h + 1]].b,a[q[h]].b)) h ++ ; 48 if (h > t) continue; 49 Max (f[a[i].b],f[a[q[h]].b] + a[i].a - (a[i].b - a[q[h]].b) * (a[i].b - a[q[h]].b - 1) / 2); 50 } 51 sort (a + mid + 1,a + r + 1,com1); 52 Divide (mid + 1,r); 53 } 54 signed main () { 55 freopen ("skip.in","r",stdin); 56 freopen ("skip.out","w",stdout); 57 n = read(); 58 for (I i(1);i <= n; ++ i) 59 a[i] = (L){read(),i}; 60 for (I i(1);i <= n; ++ i) 61 f[i] = a[i].a - i * (i - 1) / 2; 62 sort (a + 1,a + n + 1,com1); 63 Divide (1,n); 64 for (I i(1);i <= n; ++ i) 65 Max (ans,f[i] - (n - i + 1) * (n - i) / 2); 66 Max (ans,-(n + 1) * n / 2); 67 printf ("%lld\n",ans); 68 }
T4:
向量类问题的一种思路:
首先向量问题套路的转化到平面直角坐标系考虑,于是问题等价于从平面直角坐标系中
选择若干向量使得其组成树结构并满足投影和最大
(首先平面直角坐标系有关方向/斜率问题通常都可以根据数据范围设置增量通过暴力枚举所有方向计算)
根据Kruskal的原理可以想到,求最大生成树事实上并不需要考虑实际大小,我们只需要考虑相对大小并取最优选择
考虑如何优化枚举,显然问题关键点就在于相对大小,那么突破点显然在于相等时刻
考虑如何求两向量相等的分界点,根据x1 * cos + y1 * sin = x2 * cos + y2 * sin即可得出
接下来证明所有向量形成的分界点中任意两分界点所夹成的范围内任选一条向量l
所有向量在l上的投影相对大小不变:
反证法即可:若相对大小不同,那么由于相对大小的变化一定是连续的,因此必然可以再引出一条分界点
与原命题不符,因此成立
于是所有分界点将平面直角坐标系划分为若干区域,每一区域中所有向量相对大小不变,也就是说,
所有区域就可以枚举所有可能的相对大小,于是在每个区间作Kruskal即可
代码如下:
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 D double 8 #define LL long long 9 #define P pair<I,I> 10 #define MP make_pair 11 #define fir first 12 #define sec second 13 #define lowbit(x) (x & -x) 14 const I N = 55,M = 205; 15 const D Pi = acos (-1); 16 I n,m; 17 D ans; 18 struct E {I u,v,a,b; D len;} Edge[M]; 19 vector <D> Ang; 20 inline I read() { 21 I x(0),y(1); C z(getchar ()); 22 while (!isdigit(z)) { if (z == '-') y = -1; z = getchar(); } 23 while ( isdigit(z)) x = x * 10 + (z ^ 48), z = getchar(); 24 return x * y; 25 } 26 inline V Max (D &a,D b) { a = a > b ? a : b; } 27 inline V Min (I &a,I b) { a = a < b ? a : b; } 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 inline B com (const E &a,const E &b) { return a.len > b.len; } 32 inline D Figure (I a,I b) { 33 return atan ((1.0 * Edge[b].b - Edge[a].b) / (1.0 * Edge[a].a - Edge[b].a)); 34 } 35 struct DJS { 36 I f[N]; 37 inline V initital () { 38 for (I i(1);i <= n; ++ i) f[i] = i; 39 } 40 I get (I x) { 41 return x == f[x] ? x : f[x] = get (f[x]); 42 } 43 inline B merge (I x,I y) { 44 I fx (get (x)), fy (get (y)); 45 if (fx == fy) return false; 46 f[fy] = fx; return true; 47 } 48 }DJS; 49 inline V Kruskal (D ang) { 50 I a(0),b(0); DJS.initital (); 51 for (I i(1);i <= m; ++ i) 52 Edge[i].len = 1.0 * Edge[i].a * cos (ang) + 1.0 * Edge[i].b * sin (ang); 53 sort (Edge + 1,Edge + m + 1,com); 54 for (I i(1);i <= m; ++ i) 55 if (DJS.merge (Edge[i].u,Edge[i].v)) 56 a += Edge[i].a, b += Edge[i].b; 57 Max (ans,sqrt (1.0 * a * a + 1.0 * b * b)); 58 } 59 signed main () { 60 n = read(), m = read(); 61 for (I i(1);i <= m; ++ i) { 62 Edge[i].u = read(), Edge[i].v = read(); 63 Edge[i].a = read(), Edge[i].b = read(); 64 } 65 for (I i(1);i <= m; ++ i) 66 for (I j(i + 1);j <= m; ++ j) 67 Ang.push_back (Figure (i,j)), Ang.push_back (Figure (i,j) + Pi); 68 sort (Ang.begin (),Ang.end ()); 69 for (auto y : Ang) Kruskal (y); 70 printf ("%.6lf",ans); 71 }
关键点在于相对大小以及相等的突破点,注意三角函数反三角函数的应用
注意关键点,造Hack数据,投影的计算