P2962 [USACO09NOV]Lights G
洛谷
题意:
给定n个点,初始值为0,m条无向边,每次操作一个点,将自身和于自己相连的的点1变0、0变1,问最少操作次数,使得全为1
思路:
我看大佬们都是高斯消元+dfs写的,奈何我不会
贪心:每个点最多操作一次
暴力搜索:先考虑暴力搜索每种状态,时间复杂度就是2^n,这里n最大35,很明显会超时
双向搜索:那么我们使用双向搜索时,就会将时间复杂度减低很多,达到大概n*2^(n/2),这个降低是很多的
技巧就是先定义f数组用来记录第i个点可以到达的点,用或运算就可以实现,把他详细成一个二进制数就可以了,n最大35,所以开long long
把前一半搜索的结果放入哈希中,在后一半搜索中找哈希,如果恰好匹配,就是答案
如果想很轻松看懂下面代码,可以先了解一下二进制和位运算
#include <iostream>
#include <algorithm>
#include <map>
#include <unordered_map>
#include <cstring>
using namespace std;
typedef long long ll;
inline int read(void)//读入
{
register int x = 0;
register short sgn = 1;
register char c = getchar();
while (c < 48 || 57 < c)
{
if (c == 45)
sgn = 0;
c = getchar();
}
while (47 < c && c < 58)
{
x = (x << 3) + (x << 1) + c - 48;
c = getchar();
}
return sgn ? x : -x;
}
inline void write(ll x)//输出
{
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
const int N = 40;
ll f[N];
unordered_map<ll, int> mp;
int main()
{
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
int n = read(), m = read();
int res = 0x7fffffff;//结果
for (int i = 0; i < n; ++i)
f[i] = (1ll << i);//每个点都可以到自己
for (int i = 1; i <= m; ++i)
{
int u = read(), v = read();
--u, --v;//二进制从一位开始,每个数减一,再进行或运算,不懂的可以在纸上画一遍,就明白了
f[u] |= (1ll << v);//u可以到达v这个点
f[v] |= (1ll << u);//v可以到达u这个点
}
for (int i = 0; i < (1 << (n >> 1)); ++i)//双向搜索的核心,先搜索前半部分状态,对前半部分点操作
{
ll t = 0;//记录临时状态
int cnt = 0;//记录临时结果
for (int j = 0; j < (n >> 1); ++j)
{
if ((i >> j) & 1)//如果选了第j个
{
t ^= f[j];//就异或
++cnt;
}
}
if (!mp.count(t))//最后存哈希表
mp[t] = cnt;
else
mp[t] = min(mp[t], cnt);
}
for (int i = 0; i < (1 << (n - (n >> 1))); ++i)//后半部分状态
{
ll t = 0;
int cnt = 0;
for (int j = 0; j < (n - (n >> 1)); ++j)
{
if ((i >> j) & 1)
{
t ^= f[(n >> 1) + j];//由于是从0开始,但是这次搜索是后半部分,所有(n >> 1) + j表示我们对剩下的点的操作
++cnt;
}
}
if (mp.count(((1ll << n) - 1) ^ t))//如果能恰好匹配,更新最小值
res = min(res, cnt + mp[((1ll << n) - 1) ^ t]);
}
write(res);//输出答案
return 0;
}
如果在比赛的时候想不出来怎么办,还是有办法的,参照洛谷.AuCloud的玄学算法
随机化
前半部分和双向搜索一样,加一个a数组记录序号用来随机打乱,每次随机打乱一次,然后通过sa()函数从前往后遍历操作点判断是否满足条件
如果有满足的,记录最小值并退出,如果遍历完都不可以,那么返回最大值,并重新打乱
此方法极其玄学,和模拟退火有点相似,但是没有模拟退火的精髓,不过够用了,如果在比赛中,能A一道算一道了,按测试点给分的话,那就更好了
看代码注释吧
#include <iostream>
#include <algorithm>
#include <cstring>
#include <random>
#include <ctime>
using namespace std;
typedef long long ll;
inline int read(void)//读入
{
register int x = 0;
register short sgn = 1;
register char c = getchar();
while (c < 48 || 57 < c)
{
if (c == 45)
sgn = 0;
c = getchar();
}
while (47 < c && c < 58)
{
x = (x << 3) + (x << 1) + c - 48;
c = getchar();
}
return sgn ? x : -x;
}
inline void write(ll x)//输出
{
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
const int N = 40;
ll f[N];
int a[N];
int n, m;
int sa()//求当前结果
{
ll ans = 0;
for (int i = 0; i < n; ++i)
{
ans ^= f[a[i]];
if (ans == (1ll << n) - 1)//如果可以操作使得所有点都是1
return i + 1;//i是从0开始,所有加1
}
return 0x7fffffff;
}
int main()
{
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
srand(time(NULL));//随机种子,能不能过就看他了
n = read(), m = read();
int res = 0x7fffffff;
for (int i = 0; i < n; ++i)
{
a[i] = i;//记录编号,用于随机打乱
f[i] = (1ll << i);
}
for (int i = 1; i <= m; ++i)
{
int u = read(), v = read();
--u, --v;
f[u] |= (1ll << v);
f[v] |= (1ll << u);
}
int qaq;
if (n == 35)//卡时间,不超时
qaq = 1000000;
else
qaq = 1900000;
for (int i = 1; i <= qaq; ++i)
{
random_shuffle(a, a + n);//随机打乱顺序
int qwq = sa();//判断当前情况是否符合条件
res = min(res, qwq);//更新最小值
}
write(res);
return 0;
}
当然,这是你就发现,你并没有拿到100,可能是80多分,也可能90多,毕竟玄学,看你的种子了。这里也是并不推荐,不到万不得已,别乱用
这里是我试了很多次,真的不推荐,如果是过测试点,那还是很不错