二分图博弈学习笔记

news2024/12/23 4:11:14

前言:最近每场训练赛都有博弈题,而且我都被薄纱了。。。真烦

二分图博弈是少有的直接跟图论挂钩的一种博弈模型

一个博弈是二分图博弈应当满足一下条件:

  • 博弈人数为两人,轮流操作

  • 博弈状态转移可以表示成一张二分图

  • 不可访问已经访问过的状态

  • 无法转移者负

拥有二分图这么良好的性质,该博弈模型自然而然的有一个很简单的结论:如果先手状态,我们记为P,一定在二分图的最大匹配中,那么先手必胜。否则先手必败。

那么我们来看看证明:

 假设当前P不在最大匹配中,那么先手移动之后P一定会到达一个匹配点,否则我们就有了一个新的匹配,与最大匹配这一点矛盾。所以此时我们的局面是当前处于匹配点上,后手先行动。后手走的下一个点也一定是匹配点,否则就跟之前的路径形成了一条增广路,同样矛盾。所以接下来双方都是在匹配点上转移。显然最终移动步数为偶数,故此时后手必胜。

若P在最大匹配中,则只要仿照上述策略,就是一个必胜的局面。

如果P没有可以转移的局面,同样也是不在最大匹配中,当然也是必败。


由此,对于一个二分图博弈,只要我们能够判断先手所在局面是否处在二分图的最大匹配上,即可判断必胜状态。

如何判断一个点是否一定在最大匹配上?比较经典的套路就是先将其从图中移除,然后再加进来,看看是否还能对匹配有贡献。如果有的话,说明其一定处在最大匹配上。

具体实现,我们可以采用网络流。在建图的时候先不将P与源点或者汇点连接,这样跑网络流的时候就不会将其考虑进去。然后把P与源点/汇点连接,利用残量网络,再做一遍网络流。如果值非0,就是有贡献的。

但是该方法仅适用于单起点博弈 。如果需要判断的局面过多,显然时间复杂度无法接受。

如果想要找出所有局面中的必胜局面,我们有更好的办法。

找出所有必胜局面,也就是找出所有一定处在最大匹配的点,相当于找出所有不一定在最大匹配上的点。

非匹配边1->5和2->5匹配边可以互换最大匹配数不变,所以点1,2都不一定在最大匹配中。在该图的残量网络中,1->5的流量为1,匹配边5->2的反向流量为1,因此我们判断两条边可以互换。将与源点相连的边颜色col设为1,汇点设为0,dfs即可。这是判断与左部点的,判断右部点同理。

但是,但是,并不是所有满足二分图博弈模型的题目都一定得通过网络流来求解,因为不同博弈模型本身有其独特的性质,可能可以通过其他途径求解,比如23牛客多校2 I Link with Gomoku

然后就到了喜闻乐见的例题时间

[COCI2017-2018#5] Planinarenje

大意:

模板题 

不难发现,判负的条件与一方无法行动是等价的,所以该问题完美符合二分图博弈模型。然后又因为要对所有先手局面判断是否必胜,那么我们应该采用第二种方法。

code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
const ll maxn=2e5+10,maxm=2e5+10;
const ll inf=0x3f3f3f3f;
struct MaximumFlow
{
    //maxn 最多点数 maxm 最多边数
    int h[maxn],e[maxm],ne[maxm],f[maxm],idx;
    int d[maxn],cur[maxn];
    int n,m,S,T,mxn;
    ll col[maxn],ans[maxn];
    ll flag=0;
    void init(int n,int _s,int _t)
    {
        mxn=n,idx=0,S=_s,T=_t;//mxn表示最大点的大小(包括S,T),用于初始化处理,_s表示源点,_T表示汇点
        // memset(h,-1,sizeof h);
        while(n>=0)
            h[n--]=-1;
    }
    void addedge(int a,int b,int c)
    {
        ne[idx]=h[a],e[idx]=b,f[idx]=c,h[a]=idx++;
        ne[idx]=h[b],e[idx]=a,f[idx]=0,h[b]=idx++;
    }
    bool bfs()
    {
        // memset(d,-1,sizeof d);
        for(int i=0;i<=mxn;++i) d[i]=-1;
        d[S]=0;
        queue<int>q;
        q.push(S);
        cur[S]=h[S];
        while(q.size())
        {
            int t=q.front();
            q.pop();
            for(int i=h[t];i!=-1;i=ne[i])
            {
                int j=e[i];
                if(d[j]==-1&&f[i])
                {
                    d[j]=d[t]+1;
                    cur[j]=h[j];
                    if(j==T)return true;
                    q.push(j);
                }
            }
        }
        return false;
    }
    int find(int u,int limit)
    {
        if(u==T)return limit;
        int flow=0;
        for(int i=cur[u];i!=-1&&flow<limit;i=ne[i])
        {
            cur[u]=i;
            int j=e[i];
            if(d[j]==d[u]+1&&f[i])
            {
                int t=find(j,min(f[i],limit-flow));
                if(!t)d[j]=-1;
                f[i]-=t,f[i^1]+=t,flow+=t;
            }
        }
        return flow;
    }
    int dinic()
    {
        int r=0,flow;
        while(bfs())while(flow=find(S,inf))r+=flow;
        return r;
    }
    void dfs(int u,int t)
    {
        // vis[u]=1;
        if(col[u]==t)
        {
            flag=1;
            ans[u]=1;
        }
        for(int i=h[u];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(f[i]==t&&!ans[j])
            {
                dfs(j,t);
            }
        }
    }
    ll judge()
    {
        dinic();
        dfs(S,1);dfs(T,0);
        return flag;
    }

}f;
ll n,m;
ll S,T;
ll a,b;
void solve()
{
    cin>>n>>m;
    S=n*2+1;T=n*2+2;
    f.init(T+1,S,T);
    while(m--)
    {
        cin>>a>>b;
        f.addedge(a,b+n,1);
    }
    for(int i=1;i<=n;++i)
    {
        f.addedge(S,i,1);
        f.col[i]=1;
        f.addedge(i+n,T,1);
    }
    f.judge();
    for(int i=1;i<=n;++i)
    {
        if(f.ans[i]==0) cout<<"Slavko"<<endl;
        else cout<<"Mirko"<<endl;
    }

}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

20ccpc长春H

大意:
有m个数位的数字锁,存在若干状态不允许访问,也不允许访问之前访问过的节点,每次操作改变某一位,无法操作者负。给定锁的初始状态,问必胜状态。

思路:
显然锁的状态是一张二分图,因为数位和奇偶性相同的状态显然无法一次到达。那么我们只要把非法的状态不加入图中,然后采用第一种判断方法就可以了。

code

#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll INF=1e8;
const ll inf=1e8;
const int maxn=1e6+10,maxm=9e6+10;
const ll N=1e6+10;


struct MaximumFlow
{

    int h[maxn],e[maxm],ne[maxm],f[maxm],idx;
    int d[N],cur[N];
    int n,m,S,T,mxn;
    void init(int n,int _s,int _t)
    {
        mxn=n,idx=0,S=_s,T=_t;
        // memset(h,-1,sizeof h);
        while(n>=0)h[n--]=-1;
    }
    void addedge(int a,int b,int c)
    {
        ne[idx]=h[a],e[idx]=b,f[idx]=c,h[a]=idx++;
        ne[idx]=h[b],e[idx]=a,f[idx]=0,h[b]=idx++;
    }
    bool bfs()
    {
        memset(d,-1,sizeof d);
        d[S]=0;
        queue<int>q;
        q.push(S);
        cur[S]=h[S];
        while(q.size())
        {
            int t=q.front();
            q.pop();
            for(int i=h[t];i!=-1;i=ne[i])
            {
                int j=e[i];
                if(d[j]==-1&&f[i])
                {
                    d[j]=d[t]+1;
                    cur[j]=h[j];
                    if(j==T)return true;
                    q.push(j);
                }
            }
        }
        return false;
    }
    int find(int u,int limit)
    {
        if(u==T)return limit;
        int flow=0;
        for(int i=cur[u];i!=-1&&flow<limit;i=ne[i])
        {
            cur[u]=i;
            int j=e[i];
            if(d[j]==d[u]+1&&f[i])
            {
                int t=find(j,min(f[i],limit-flow));
                if(!t)d[j]=-1;
                f[i]-=t,f[i^1]+=t,flow+=t;
            }
        }
        return flow;
    }
    int dinic()
    {
        int r=0,flow;
        while(bfs())while(flow=find(S,inf))r+=flow;
        return r;
    }
}f;
ll n,m,pwd;
ll vis[N];
ll c[]={1,10,100,1000,10000,100000};
ll gt(ll x)
{
    ll sum=0;
    while(x)
    {
        sum+=x%10;
        x/=10;
    }
    return sum%2;
}
void solve()
{
    cin>>m>>n>>pwd;
    memset(vis,0,sizeof vis);
    for(int i=1;i<=n;++i)
    {
        ll a;cin>>a;vis[a]=1;
    }
    ll up=1;for(int i=1;i<=m;++i) up*=10;
    ll S=up,T=up+1;
    f.init(up+5,S,T);
    for(int i=0;i<up;++i)
    {
        //判断pwd是否一定在二分图的最大匹配中,一开始不将其与S或T连边
        //但是与网络其他节点相连。跑完网络流之后将pwd与对应起点/终点相连。
        //利用残量网络再跑一遍网络流。如果值大于0,代表其一定在二分图的最大匹配中
        if(vis[i]) continue;
        if(gt(i))
        {
            if(i!=pwd) f.addedge(i,T,1);
            continue;
        }
        if(i!=pwd) f.addedge(S,i,1);

        ll x;
        for(int j=0;j<m;++j)
        {
            x=i;
            int num=i;
            x/=c[j];
            x%=10;
            num=num-x*c[j];
            ll t1=num+((x+1)%10)*c[j];
            f.addedge(i,t1,1);
            t1=num+((x-1+10)%10)*c[j];
            f.addedge(i,t1,1);
        }
    }
    f.dinic();
    if(gt(pwd)) f.addedge(pwd,T,1);
    else f.addedge(S,pwd,1);
    if(f.dinic()) cout<<"Alice"<<endl;
    else cout<<"Bob"<<endl;
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    ll t;cin>>t;while(t--)
    solve();
    return 0;
}

[NOI2011] 兔兔与蛋蛋游戏

大意:

懒得写了,自己去读吧

思路:
有幸做出人生第二道黑题(敲下来有阻塞,但没想象中那么费劲)

我们稍微转化一下,将黑白棋的移动看成空格的移动。先手操作的时候,空格只能与白棋交换,此空格相当于黑棋。后手操作的时候,空格只能与黑棋交换,此时空格相当于白棋。所以相当于轮流在黑白棋之间切换,每次只能与不同颜色的点交换位置,显然是一张二分图。此外有一个性质比较隐蔽,那就是我们无法访问之前访问过的局面。因为黑白棋是轮流操作的,黑棋走过的点白棋是无法走的,反之同理。由此,我们成功转化成了二分图博弈,

这题要求我们输出所有操作失误的步骤。其实就是操作前后的都处于必胜状态的点。(仔细想想,不难理解)。每次操作之后整张图都不一样了,我们要判断的起始点也不一样了,所以我们每次要重新建图,因为图比较小,所以该方法是可行的。(但是还是要注意一下常数,本人吸了氧才过www)

code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=110;
const ll INF=0x3f3f3f3f,inf=INF;
const ll maxn=4000,maxm=maxn*20;
struct MaximumFlow
{
    //maxn 最多点数 maxm 最多边数
    int h[maxn],e[maxm],ne[maxm],f[maxm],idx;
    int d[maxn],cur[maxn];
    int n,m,S,T,mxn;
    ll mp_vis[maxn];
    ll col[maxn],ans[maxn];
    ll flag=0;
    void init(int n,int _s,int _t)
    {
        mxn=n,idx=0,S=_s,T=_t;//mxn表示最大点的大小(包括S,T),用于初始化处理,_s表示源点,_T表示汇点
        // memset(h,-1,sizeof h);
        while(n>=0)
            h[n--]=-1;
    }
    void addedge(int a,int b,int c)
    {
        ne[idx]=h[a],e[idx]=b,f[idx]=c,h[a]=idx++;
        ne[idx]=h[b],e[idx]=a,f[idx]=0,h[b]=idx++;
    }
    bool bfs()
    {
        // memset(d,-1,sizeof d);
        for(int i=0;i<=mxn;++i) d[i]=-1;
        d[S]=0;
        queue<int>q;
        q.push(S);
        cur[S]=h[S];
        while(q.size())
        {
            int t=q.front();
            q.pop();
            for(int i=h[t];i!=-1;i=ne[i])
            {
                int j=e[i];
                if(d[j]==-1&&f[i])
                {
                    d[j]=d[t]+1;
                    cur[j]=h[j];
                    if(j==T)return true;
                    q.push(j);
                }
            }
        }
        return false;
    }
    int find(int u,int limit)
    {
        if(u==T)return limit;
        int flow=0;
        for(int i=cur[u];i!=-1&&flow<limit;i=ne[i])
        {
            cur[u]=i;
            int j=e[i];
            if(d[j]==d[u]+1&&f[i])
            {
                int t=find(j,min(f[i],limit-flow));
                if(!t)d[j]=-1;
                f[i]-=t,f[i^1]+=t,flow+=t;
            }
        }
        return flow;
    }
    int dinic()
    {
        int r=0,flow;
        while(bfs())while(flow=find(S,inf))r+=flow;
        return r;
    }
    bool judge(ll x,ll y)
    {
        for(int i=h[x];i!=-1;i=ne[i])
        {
            ll Y=e[i];
            if(Y==y) return 1;
        }
        return 0;
    }
}f;
ll n,m;
char mp[N][N];
ll px,py;
ll S,T;
vector<ll> ans;
ll gt(ll x,ll y)
{
    return y+(x-1)*m;
}
ll dir[][4]={{1,0},{-1,0},{0,1},{0,-1}};
bool inmap(ll x,ll y)
{
    return x>=1&&y>=1&&x<=n&&y<=m;
}
bool jd(ll x,ll y)
{
    if(x==px&&y==py) return 1;//是当前操作点
    return 0;
}
inline ll gt()
{
    //奇数点与汇点相连
    //偶数点与源点相连
    f.init(n*m+5,S,T);//每次要将起始点与源点/汇点断开连接,重新建图,反正图也不大
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            if(jd(i,j)) continue;//去掉操作起始点
            if((i+j)%2==0) continue;
            f.addedge(gt(i,j),T,1);
        }
    }
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            if((i+j)%2) continue;//去掉操作起始点
            if(!jd(i,j)) f.addedge(S,gt(i,j),1);
            for(int k=0;k<4;++k)
            {
                ll xx=i+dir[k][0];
                ll yy=j+dir[k][1];
                if(!inmap(xx,yy)) continue;
                if(mp[xx][yy]==mp[i][j]) continue;
                f.addedge(gt(i,j),gt(xx,yy),1);
            }
        }
    }
    f.dinic();
    if((px+py)%2) f.addedge(gt(px,py),T,1);
    else f.addedge(S,gt(px,py),1);

    return f.dinic();
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            cin>>mp[i][j];
            if(mp[i][j]=='.')
            {
                px=i;py=j;
            }
        }
    }
    


    S=n*m+1,T=n*m+2;
    ll op;cin>>op;
    for(int dx=1;dx<=op;++dx)
    {
        ll a,b;cin>>a>>b;
        mp[px][py]='X';//因为先手操作之后不能返回当前局面,所以要将这个位置置为X
        ll pre_1=gt();
        mp[px][py]='.';//
        swap(mp[px][py],mp[a][b]);
        px=a;py=b;

        mp[px][py]='O';//与上面改成X是同理的
        ll pre_2=gt();
        mp[px][py]='.';//
        // cout<<dx<<' '<<px<<' '<<py<<' '<<pre_1<<' '<<pre_2<<endl;
        if(pre_1&&pre_2)
        {
            ans.push_back(dx);
            //如果前后都是必胜态,那么该操作失误
        }
        ///
        cin>>a>>b;
        swap(mp[px][py],mp[a][b]);
        px=a;py=b;
    }

    cout<<ans.size()<<endl;
    for(auto i:ans) cout<<i<<endl;
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

留个小联系:[JSOI2009] 游戏

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/783592.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL 的 crash-safe浅谈

MySql执行流程 MySQL作为当下最流行的开源关系型数据库&#xff0c;有一个很关键和基本的能力&#xff0c;就是必须能够保证数据不会丢。那么在这个能力背后&#xff0c;MySQL是如何设计才能保证不管在什么时间崩溃&#xff0c;恢复后都能保证数据不会丢呢&#xff1f;有哪些…

在Win11的WSL子系统Ubuntu上安装Gnome桌面环境

目录 1. 使用 WSL 在 Win11 上安装 Linux 2. 安装Ubuntu 22.04默认Gnome桌面环境 2.1更新Ubuntu 22.04软件包 2.2 安装Ubuntu桌面环境 2.3 重启服务 2.4 重启Ubuntu 22.04系统 2.5 登录Gnome桌面环境 在Win11上安装ubuntu版linux系统并实现默认Gnome桌面环境&#xff08…

MySQL数据库(八)

目录 一、什么是索引 1.1索引的原理 1.2索引的优缺点 二、索引的使用 2.1查看索引 2.2手动创建索引 2.3删除索引 三、MySQL索引底层的数据结构 3.1 B树 3.2 B树 一、什么是索引 索引是一种特殊的文件&#xff0c;包含着对数据表里所有记录的引用指针。可以对表中的一列或多列创…

深度理解 Spring AOP

一、什么是AOP(面向切面编程)&#xff1f;&#x1f349; AOP 为 Aspect Oriented Programming 的缩写&#xff0c;意思为面向切面编程&#xff0c;是通过预编译方式 和运行期 动态代理 实现程序功能的统一维护的一种技术。 AOP &#xff08;面向切面编程&#xff09;是 OOP&a…

Jmeter 接口自动化和 Python 接口自动化,到底选哪个?

目录 前言&#xff1a; 背景 Jmeter 接口自动化 特点 Python 接口自动化 特点 谈项目 写在最后 前言&#xff1a; JMeter接口自动化和Python接口自动化都是常见的选择。 背景 很多刚接触接口自动化的朋友都会疑惑&#xff0c;市面上 Jmeter 接口自动化&#xff0c;Py…

题目2 文件上传(保姆级教程)

url&#xff1a;http://192.168.154.253:82/ #打开http://XXX:81/&#xff0c;XXX为靶机的ip地址 审题 1、打开题目看到有一个提示&#xff0c;此题目需要绕过WAF过滤规则&#xff0c;上传木马获取webshell&#xff0c;最后从根目录下key.php文件中获得flag 2、开始答题 第一步…

【数据结构】二叉树详解(3)

⭐️ 前言 ✨ 往期链接&#xff1a;【数据结构】二叉树详解(1) 在第一篇二叉树文章中&#xff0c;我们探讨了二叉树的链式结构定义与实现。二叉的遍历包含( 前序/中序/后序遍历 )及代码实现和递归流程图的详细讲解。还有一些二叉树的其他接口定义与实现&#xff0c;包含 Binar…

基于netlify生成custom SSL certificate

&#xff08;1&#xff09;腾讯云申请 &#xff08;2&#xff09;域名控制台解析 &#xff08;3&#xff09;Nginx下载&#xff08;crt: CA certificate Chain)

C++ 设计模式 ---- 接口隔离模式

“接口隔离”模式 在组件构建过程中&#xff0c;某些接口之间直接的依赖常常会带来很多问题、甚至根本无法实现。采用添加一层间接&#xff08;稳定&#xff09;接口&#xff0c;来隔离本来互相紧密关联的接口是一种常见的解决方案。典型模式&#xff1a;1、Facade2、Proxy3、…

MongoDB原生语句更新嵌套数组的值

一、更新一层嵌套数组 首先执行MongoDB原生语句脚本在user集合中产生一些样本数据,如下所示: db.user.insert({"_id":1,"title":"爱情公寓3","students":[{"student_id":1001,"student_name":"林宛瑜&quo…

Docker介绍及安装使用

Docker介绍及安装使用 一、Docker的概述1、Docker是什么&#xff1f;2、Docker的Logo3、Docker的设计宗旨&#xff08;一次封装&#xff0c;到处运行&#xff09;4、容器化越来越受欢迎的原因 二、Docker与虚拟机的区别三、Docker的使用场景四、Docker的核心概念1、镜像2、容器…

基于linux下的高并发服务器开发(第二章)- 2.25 sigprocmask 函数使用

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);- 功能&#xff1a;将自定义信号集中的数据设置到内核中&#xff08;设置阻塞&#xff0c;解除阻塞&#xff0c;替换&#xff09;- 参数&#xff1a;- how : 如何对内核阻塞信号集进行处理SIG_BLOCK: 将用户设…

【MySQl】MySQl中的乐观锁是怎么实现的

文章目录 前言一、乐观锁二、如何实现乐观锁呢&#xff0c;一般来说有以下2种方式2.1、使用数据版本&#xff08;Version&#xff09;记录机制实现2.2、乐观锁定的第二种实现方式和第一种差不多 前言 mysql中的乐观锁是怎么实现的&#xff1f;很多新手对此不是很清楚&#xff…

第一次参加【CSDN周赛(考试/编程竞赛)】第65期,应该注意些什么?都考什么题目?要具备什么知识?耗时__,我居然取得了__分的成绩

订阅专栏,学习更多干货知识!! 第一次参加 CSDN里的竞赛(考试),都需要注意些什么?考试都考了什么?要具备什么知识?本文带你了解一下!! 🤾🏿‍♂️目录 🌁一、先来看结果吧(有Bug?)🥕1.1 什么情况!🥕1.2 测评报告🥤1.2.1 选择题🥤1.2.2 编程题🥕1…

Redis持久化:分别启用rdb和aof,并查看是否有对应文件生成

一、rdb 简介&#xff1a;在指定的时间间隔内将内存中的数据集快照写入磁盘&#xff0c; 也就是Snapshot快照&#xff0c;它恢复时是将快照文件直接读到内存里。 1. 进入redis.conf文件中查看配置文件 [rootserver ~]# vim /usr/local/redis-stable/redis.conf 2.把持久化的…

《Docker数据管理:卷、挂载和持久化,保障容器环境数据安全》

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

【博客682】k8s apiserver bookmarks机制以更高效检测变更

k8s apiserver bookmarks机制以更高效检测变更 list-watch背景&#xff1a; List-Watch 是kubernetes中server和client通信的最核心的机制&#xff0c; 比如说api-server监听etcd&#xff0c; kubelet监听api-server&#xff0c; scheduler监听api-server等等&#xff0c;其实…

Paragon NTFS2023最新版Mac读写NTFS磁盘工具

Paragon NTFS for Mac是Mac平台上一款非常优秀的读写工具&#xff0c;可以在Mac OS X中完全读写、修改、访问NTFS硬盘、U盘等外接设备的文件。这款软件最大的亮点简书可以让我们读写 NTFS 分区&#xff0c;因为在Mac OS X 系统上&#xff0c;默认状态下我们只能读取NTFS 分区&a…

152. 乘积最大子数组

152. 乘积最大子数组 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a; 原题链接&#xff1a; 152. 乘积最大子数组 https://leetcode.cn/problems/maximum-product-subarray/ 完成情况&#xff1a; 解题思路&#xff1a; 看好题目&…

Nginx 301重定向分析

参考; 404 - 墨天轮 深度硬核文:Nginx的301重定向处理过程分析 - 知乎 Nginx的301状态码处理逻辑设计 HTTP协议中3xx开头的状态响应码都是表示重定向的响应。根据RFC的定义&#xff1a; 301 Moved Permanently 302 Found 303 See Other 307 Temporary Redirect 301是永…