1 条题解

  • 0
    @ 2025-8-24 21:32:38

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar world_execute
    I swear I'll follow through.

    搬运于2025-08-24 21:32:38,当前版本为作者最后更新于2019-02-05 16:36:23,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    [ 更好的阅读体验 ]

    [ 原题面 ]


    第零部分 —— 阅读题目

    • 题目大意:有 M*N 个格子,一面是白色,另一面是黑色。给你一个操作,翻转某个位置,使这个位置和与它有公共边的所有格子变换一次颜色(黑->白,白->黑)。现在,给你一个初始状态,问你如何翻转,才能使翻转次数最小,如有多个解,输出字典序最小的那个,如果无解,输出“IMPOSSIBLE"(忽略引号)

    • 数据范围:1 ≤ M, N ≤ 15

    • So,看到这个数据范围,是不是觉得出题人很良心,但在看完题目之后,发现 2^30 太大了 (连int都快存不下了) 几乎无法得分


    第一部分 —— 开始分析

    • 首先,我们可以从数据范围知道,单纯地去枚举每个位置的翻转情况是一定过不了全部的测试点的

    • 这样枚举的时间复杂度约是 O(2^(n+m)),因为总共 n*m 个点,每个点可以反转或者不反转

    • So,上面的时间复杂度是我们无法接受的,必须降下来


    第二部分 —— 初现思路

    • 通过题目我们可以知道,我们要把所有点反转成白色

    • So,我们要尽可能地让某一部分都变成白色,然后在保证之前的白色部分不会缩小的条件下,慢慢地扩大,直到覆盖整个棋盘 ( 有没有一点像转魔方? )


    第三部分 —— 深入思考

    • 通过上面的分析,我想出一种方法 (当然,没看过题解 QAQ):还是枚举 (善变的我)

    • 先枚举第一行(其实枚举任何一行,或任何一列都可以得出结果,见延伸阅读① )

    • 再根据之前说的:“我们要慢慢扩大,直到覆盖整个棋盘” ,但是这句话的根本条件是保证白色部分不会缩小。

    • So,如何保证白色区域不会缩小,是这道题的突破口


    第四部分 —— 解决问题

    • (这一部分的标题是不是很像大部分数学试卷的最后一大题?)

    • 接着,分析题目,每一次反转,都会把一个“十”字形区域取反,it looks like this:

         ...+---+---+---+...
         ...|   | * |   |...
         ...+---+---+---+...
         ...| * | * | * |...
         ...+---+---+---+...
         ...|   | * |   |...
         ...+---+---+---+...
          (星号标注区取反)
           Tips: 取反  1->0  0->1 ,相当于 not 或 ! 
           		 但是代码中,本人喜欢使用 1-X 来取反
      
    • (新奇的画风)

    • 如果哪一格不是0的话,我们就可以在它的下方进行反转。(具体为什么是下方,见 延伸阅读① )

    • 使用这样的方法,除了第一行以外,我们都可以解决,只用在完成之后,判断一下,最后一行是不是都是0,如果是,我们就得出了一个答案,在加上一些判断,这部分代码就完成了

    • 在这一部分代码完成是,我们还需要注意的是,每次都要出现一个Shadow数组,在每次操作 (骚操作?) 之前,必须保存现场,(Sounds like a search algorithm),在最后还原。

    • So,在枚举时,我们用搜索解决好了 (这更体现出了我的善变, o(////▽////)q ) ,在加上前面的思路,与输入输出,代码就可以开始打啦。<( ̄︶ ̄)↗[GO!]


    第五部分 —— 代码实现

    • 输入部分,我相信大家都写得出啦

    • 反转部分,这十分的简单,但是要注意如果使用坐标位置不要搞混

    • 搜索部分,万一有比我还弱的蒟蒻的话,我只好写出来了:

    void Work(int k)
    {
    	if (!k) Doit();              // 全部枚举好了
    	else
    	{
    		Work(k - 1);               // 递归不反转
    		Reversal(1, k), ++b;       // 翻转一下,用来递归翻转情况
    		Work(k - 1);               // 递归翻转
    		Reversal(1, k), --b;       // 翻转一下,用来还原
    	}
    }
    
    • 这个函数写的不是特别美观 (如果有 int main(){} 这样的函数就好了) 但也可以凑合看看

    • 其中的 k 表示还有多少个位置没有选,b表示翻转了多少次,因为需要输出最少翻转次数

    • 也许有的同学会问:为什么不用“选了多少个位置”来表示k呢?为了代码短,如果你有自己的习惯,请不要学本人

    • Doit,这只需要把思路翻译成代码就好了,但是我还是觉得出示一下 代码更好一点:

    void Doit()
    {
    	Map = Shadow
    	For (i = 1..n-1)
    		For (j = 1..m)
    			If (Map[i][j])
    				Reversal(i+1,j),++p;//注意i从1到n-1
    	Flag = 1;
    	for (int j=1..m; Flag&=(!Map[n][j++]));     //判断最后一行是不是全为0
    	if ((p+b<D || (p+b==D && Judger()) || D==-1) && Flag)
    	{
    		Ans = ans
    		D = p+b
    	}                                           //判断是否符合替换要求
    	Shadow = Map
        Ans[2][1] ~ Ans[n][m] = 0
    }
    
    • 其中D表示目前最优解的反转次数,Judger表示与当前解进行字典序比较,其他就自己去理解吧。 (>▽<)

    • 输出,相信你们会写的比本人好的,我就不浪费笔墨了,主要就是无解时需要输出“IMPOSSIBLE”需要注意

    • So,第五部分的代码实现就解决了,是不是觉得还比较简单,还有本人第一次在洛谷发布题解,不知道如何写才好,如果有没讲清楚的地方,尽管在评论中回复,我尽量做到有问必答。


    第六部分 —— 后记?

    • 管理员大大要体谅我这个大年初一在家一个人码代码的人 (空巢寂寞老人???) 我已经尽力把这篇题解写好了,如果还有什么地方写的不好的 (因为我们是为人民服务的QAQ) ,我会继续改进的

    • 当然,这不是真正的后记,还有延伸阅读没有写呢

    • So,大家所期望的延伸阅读就成了第七部分 (也许只有我期望)


    第七部分 —— 延伸阅读①

    • 当然,并没有延伸阅读②

    • 你可以选择任何一行或一列

    • 如果你选择某一行,但是它不是第一行,或最后一行的话,那么每个数字,不管你想把他从“1”变成“0”,还是从“0”变成“0”,都会出现两种方案

    • 因为在你想把“1”变成“0”时,可以在上方翻转,或是在下方翻转。当你想把“0”变成“0”时,有可能时上下都不翻转,或是上下同时翻转

    • 这样,时间复杂度将会飙升,因为你又需要枚举的次数要增加 2^n 次,但最后总是能得出正确答案,但时间复杂度为 O(2^(n+1)+n^2)

    • 列也一样,最好选择第一列或最后一列

    • 这样,如果不是用第一行(列)或最后一行(列)的话,会非常麻烦,代码不好实现,时间复杂度也更打大,这是得不偿失的

    • So,这就是为什么我用第一行作为起始行了 (当然,第一列也可以)


    第八部分 —— 真·后记

    • 不知不觉就写了这么多了呢,希望各位给我一个赞,或是给予我您的宝贵的意见

    • So,题解就到这了吧

    • 1

    信息

    ID
    951
    时间
    1000ms
    内存
    125MiB
    难度
    5
    标签
    递交数
    0
    已通过
    0
    上传者