NOIP 模拟十
首先谈本次考试的问题:
1>:算法拓展应用(本质认知) 2>:转化问题(问题本质分析)
之前题解中的反思过于注重对问题的探讨而忽略了问题的解决方法及应用拓展导致考场上只能抽象问题而不能解决问题
信息竞赛是一门理论与应用相结合的学问,故应兼重。对本人而言要多思考算法解决的类型问题及拓展应用领域
T1:入阵曲
首先抽象问题,问题本质上是说给定一个矩阵,其单位元均带权,求存在多少子矩阵满足子矩阵权值和被K整除
也就是说本题实际上是对范围信息的查询,那么对于范围信息的查询,我们通常考虑的是采取前缀和与差分,排序,倍增
的手段来对范围信息进行记录或预处理,从而优化算法复杂度(应该多考虑算法应用域而非仅关注抽象问题)。考虑通过
前缀和预处理后我们可以通过O(n^4)的效率枚举每个矩阵并以O(1)复杂度查询矩阵权值和。
然而我们发现以O(n^4)的效率无法通过本题,这也在提醒我们:存在一种方法能够优化枚举过程或非枚举手段,继续考虑优化。
(当问题规模复杂难以进行时,考虑归纳,即降低问题规模,试图类比推出原问题解决方案)此时考虑一维情况,枚举复杂度为O(n^2),
然而观察问题发现还有一个条件为使用,即我们并非要枚举每一个子空间计算权值和,而是要求存在多少子空间满足被K整除,考虑
从此入手去优化。当问题为一维时我们发现若只判断子区间被K整除,仅需考虑存在多少对前缀和满足在模K的意义下同余即可,
此时只需O(n)扫描前缀和并开一个桶来统计K同余类中各存在对前缀和即可
这样问题只需拓展到二维即可,同样的情况,只要满足二维前缀和关于K同余即可。此时类比一维,我们并不需完全枚举子矩阵坐标,
只需满足其处于同一前缀和下即可,这样就优化掉一维列的枚举。
代码如下:
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define I int 4 #define LL long long 5 #define C char 6 #define RE register 7 #define L inline 8 I n,m,mod,prefix[405][405],pos[405],num[1000005]; 9 LL res; 10 L I read(); 11 signed main(){ 12 n = read(); m = read(); mod = read(); 13 for(RE I i(1);i <= n; ++ i) 14 for(RE I j(1);j <= m; ++ j) 15 prefix[i][j] = (prefix[i][j - 1] + prefix[i - 1][j] + read() - prefix[i - 1][j - 1] + mod) % mod; 16 for(RE I i(0);i < n; ++ i) 17 for(RE I j(i + 1);j <= n; ++ j){ 18 num[0] = 1; 19 for(RE I k(1);k <= m; ++ k) res += num[(pos[k] = prefix[j][k] - prefix[i][k] + mod) %= mod]++; 20 for(RE I k(1);k <= m; ++ k) num[pos[k]] = 0; 21 } 22 printf("%lld",res); 23 } 24 L I read(){RE I x(0);RE C z(getchar());while(!isdigit(z))z=getchar();while(isdigit(z))x=(x<<3)+(x<<1)+(z^48),z=getchar();return x;} 25
反思对与问题算法应用的理解,多拓展思路,考虑各种算法可能的不同的用法
T2:将军令
考场试图写出神仙树形DP,然而果不其然WA,警示考场上考虑代码复杂度,合理分配时间,事实上必然存在极优做法
问题一个很重要的性质为边权为一,此时模拟过程发现满足贪心最优解,也就是说只需要从叶节点向上枚举,若当前节点未被覆盖
则在其k级祖先处安置,并由其k级祖先处更新未被覆盖的节点(dfs),预处理可以利用bfs倒序确定枚举顺序(考虑算法拓展应用)
时间复杂度O(kn)(考场上记得计算时间复杂度,不要思考不必要的优化,一可能并不存在,二可能增加代码复杂度并浪费时间)
代码如下:
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define I int 4 #define C char 5 #define RE register 6 #define V void 7 #define L inline 8 const I MAXN = 1e5 + 5; 9 I n,res,len,typ,tot,head[MAXN],h,t,q[MAXN],father[MAXN],f[MAXN]; 10 struct LYM{I to,nxt;}node[MAXN << 1]; 11 L I read(); L V store(I,I); V dfs(I); 12 signed main(){ 13 n = read(); len = read(); typ = read(); tot = 1; 14 for(RE I i(1);i < n; ++ i) store(read(),read()); 15 h = 0; t = 1; q[++h] = 1; father[1] = 1; 16 while(h <= t){ 17 I x = q[h++]; 18 for(RE I i(head[x]),y(node[i].to); i ;i = node[i].nxt,y = node[i].to) 19 if(!father[y]){ 20 father[y] = x; 21 q[++t] = y; 22 } 23 } 24 memset(f,-1,sizeof(f)); 25 for(RE I i(n); i ; -- i) 26 if(f[q[i]] == -1){ 27 res++; 28 I y = q[i]; 29 for(RE I j(len); j ; -- j) y = father[y]; 30 f[y] = len; 31 dfs(y); 32 } 33 printf("%d",res); 34 } 35 L I read(){RE I x(0);RE C z(getchar());while(!isdigit(z))z=getchar();while(isdigit(z))x=(x<<3)+(x<<1)+(z^48),z=getchar();return x;} 36 L V store(I x,I y){ 37 node[++tot].to = y; node[tot].nxt = head[x]; head[x] = tot; 38 node[++tot].to = x; node[tot].nxt = head[y]; head[y] = tot; 39 } 40 V dfs(I from){ 41 if(!f[from]) return ; 42 for(RE I i(head[from]),y(node[i].to); i ;i = node[i].nxt,y = node[i].to) 43 if(f[y] < f[from] - 1){ 44 f[y] = f[from] - 1; 45 dfs(y); 46 } 47 }
T3:星空
首先序列A中存在若干个0与若干个1(假定0代表灯亮,1代表灯灭),目标是通过若干次操作使得序列所有元素均为0
每次操作可以从给定的m种长度中选择一种,将序列中任意一段长度为选定长度的子序列取反,求最有情况下至少需要多少次操作
考虑分治问题,首先根据数据范围,若在每次操作是均以O(len)复杂度对序列取反必然超时,也就是说首先需要优化取反操作
类似于T1入阵曲,区间取反也是对范围信息的查询(操作),考虑对于范围信息进行维护的两种预处理手段:前缀和与差分,
发现利用差分我们可以在O(1)复杂度对区间进行取反,那么问题主体就由原序列转移到差分序列。
长度为n的序列中,存在不超过2k个1,求使得序列所有元素均为0的最小操作次数。问题元素很少,在解决完区间取反问题后
就要考虑每次操作的效果,很显然每次操作我们可以选取区间上一定距离的两点进行取反。考虑为了是的操作次数最少,每次只需
对两个位置为1,0或1,1进行操作(因为对0,0进行操作无意义),若1,1进行操作则二者均消失,若1,0进行操作则二者互换
位置,发现问题进一步转化,在可供选择的距离下,如何经过最小操作次数使得所有1相互移动最终消失,因为只有有限种距离可
供选择,故我们发现任意两个1消失所需最小距离是一定的,通过bfs可以预处理得到。
最终问题呈现形式为,存在不超过2k个物品,已知任意2个物品消失代价,如何选择使得所有物品消失总代价最小,很显然可以
通过状压DP解决。
最终考虑一下几个问题:
首先差分序列所有为0如何保证原序列全为0而非全为1(其实可以模拟,但思考原理更重要)。考虑差分序列对于本题的意义,其
代表原序列的不同性,一个被我们忽视的重要潜条件为:在进行差分时我们默认0号灯亮!这意味着从0号灯后若差分序列为1则代表
灯灭。此时在默认0号灯亮与差分意义的双重制约下保证操作完成时原序列为0;其实也可以考虑为在保证最优情况下,由于主观参与会
保证只对1进行操作,但是貌似并不严谨。
第二考虑时间复杂度优化问题,也是常见的最优化问题的等效手段。在最后进行DP是我们显然可以k^2枚举所有情况取min,然而
时间复杂度并不保险,考虑问题仅需求出最终最优结果,我们并不需要保证每个状态的最优性,因此我们只需要使得问题不断向最终结果
拓展即可。另一种理解为,在状态转移时发现有很多冗余情况,为了优化复杂度,我们在枚举时进枚举当前状态对以后第一个造成影响的
状态转移即可,可以发现能枚举到所有情况,详见代码(注意理解应用最优化问题优化部分的代码):