P2962 [USACO09NOV] Lights G(双向搜索)

P2962 [USACO09NOV]Lights G

洛谷

来源: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多,毕竟玄学,看你的种子了。这里也是并不推荐,不到万不得已,别乱用
在这里插入图片描述这里是我试了很多次,真的不推荐,如果是过测试点,那还是很不错

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Miss .

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值