審題:
本題需要我們將多組測試用例中拉燈數小于等于6的最小拉燈數輸出,若拉燈數最小值仍大于6,則輸出-1思路:
方法一:二進制枚舉首先我們先分析一下基本特性:
1.所有的燈不可能重復拉:若拉的數為偶數,相當于沒拉,這會憑空讓拉燈數加n,不符合我們找最小拉燈數的需求。若拉的數為非1奇數,同理,我們可以直接拉一次達到同樣的效果,沒必要多拉那幾次
2.拉燈先后順序無影響:因為拉燈事件是相互獨立的,且拉燈最后作用在燈上就是變換次數,所以無論先拉哪個燈,最終每個燈被拉的次數不會變,最終狀態也就不會變
3.確定了第一行的拉燈情況就可以確定下一行的拉燈情況:若我們確定了第一行如何拉燈,那么為了使所有燈都處于亮的狀態,第二行的目的就是使第一行燈保持全亮,第三行的目的就是讓第二行全亮,以此類推,可以知道剩下的拉燈方法
然后我們分析一下具體的實現方法:
1.第一步:將燈的情況看成二進制數,并將其反著存到數組a中,也就是說我們的最終目的就轉換成將燈弄成全滅ps:二進制的位與數組索引對齊,第0位在最左側
2.第二步:二進制枚舉拉燈的方法,二進制中的1表示拉燈,0表示不拉燈
3.第三步:計算當前拉燈所需拉的次數并記錄在count變量中
4.第四步:計算當前行燈按完后的結果以及下一行按完后的結果
5.第五步:求出下一行的按法
6.第六步:若a[4] == 0,則維護最小拉燈數answer
解題:
#include<iostream> #include<cstring> using namespace std; int n; const int N = 10; int a[N];//用二進制形式存儲初始狀態,二進制位數和索引對齊 int f[N];//備份a int cal(int num) {int cnt = 0;while (num){cnt++;num &= num - 1;}return cnt; } int main() {cin >> n;while (n--){//多組測試,需要清空遺留痕跡memset(a, 0, sizeof a);//存儲初始數據for (int i = 0; i < 5; i++){for (int j = 0; j < 5; j++){char ch;cin >> ch;if (ch == '0') a[i] |= (1 << j);//反著存儲:目標變為讓燈全滅}}//開始二進制枚舉int answer = 0x3f3f3f;for (int p = 0; p < (1 << 5); p++){memcpy(f, a, sizeof a);int push = p;int count = 0;for (int i = 0; i < 5; i++){count += cal(push);//求出當前行按完結果f[i] = f[i] ^ push ^ (push << 1) ^ (push >> 1);//清除高位污染f[i] &= (1 << 5) - 1;//求出下一行按完結果f[i + 1] = push^f[i+1];//求出下一行按法push = f[i];}if(f[4] == 0) answer = min(answer,count);}if (answer <= 6){cout << answer << endl;}else{cout << -1 << endl;}}return 0; }
(1)f[N]數組的作用:備份數組a的情況,用于進行多次的二進制枚舉拉燈模擬
(2)存在多組數據需要判斷的時候我們一定要將全局變量的數組等痕跡清空,比如這里的數組a
(3)在存儲數據給a數組的時候:我們的目的是將值為0的位置變為值為1,所以在第j位為0的時候有a[i] |= (1 << j),會將a的第j位變為1
(4)cal的目的是計算出數據的二進制位有多少個1:核心邏輯就是每次進行循環都會將一個二進制位的1分解掉
(5)我們按燈其實就是要將當前行的被按燈本身以及其左,其右的0變為1,1變為0。而其實二進制運算中的異或可以剛好達成這個目的,因為對應位置為0/1和對應位置為1的值進行異或都會變。
而對于下一行則更簡單,只需要對被按位置改變即可
(6)下一行的按法怎么求?
由于我們的目的變為了讓燈全滅,所以上一行按完之后的值就是下一行的按法P10449 費解的開關 - 洛谷