acwing 1131 拯救大兵瑞恩 【多状态最短路】 【状态压缩】【01边权双端队列】 【增加状态法(DP分析法:从集合的角度优化)】【状态压缩位运算的细节整理】【二维编号 打表 一维编号】

1131. 拯救大兵瑞恩​​​​​​​

1944 年,特种兵麦克接到国防部的命令,要求立即赶赴太平洋上的一个孤岛,营救被敌军俘虏的大兵瑞恩。

瑞恩被关押在一个迷宫里,迷宫地形复杂,但幸好麦克得到了迷宫的地形图。

迷宫的外形是一个长方形,其南北方向被划分为 NN 行,东西方向被划分为 MM 列, 于是整个迷宫被划分为 N×MN×M 个单元。

每一个单元的位置可用一个有序数对 (单元的行号, 单元的列号) 来表示。

南北或东西方向相邻的 22 个单元之间可能互通,也可能有一扇锁着的门,或者是一堵不可逾越的墙。

注意: 门可以从两个方向穿过,即可以看成一条无向边。

迷宫中有一些单元存放着钥匙,同一个单元可能存放 多把钥匙,并且所有的门被分成 PP 类,打开同一类的门的钥匙相同,不同类门的钥匙不同。

大兵瑞恩被关押在迷宫的东南角,即 (N,M)(N,M) 单元里,并已经昏迷。

迷宫只有一个入口,在西北角。

也就是说,麦克可以直接进入 (1,1)(1,1) 单元。

另外,麦克从一个单元移动到另一个相邻单元的时间为 11,拿取所在单元的钥匙的时间以及用钥匙开门的时间可忽略不计。

试设计一个算法,帮助麦克以最快的方式到达瑞恩所在单元,营救大兵瑞恩。

输入格式

第一行有三个整数,分别表示 N,M,PN,M,P 的值。

第二行是一个整数 kk,表示迷宫中门和墙的总数。

接下来 kk 行,每行包含五个整数,Xi1,Yi1,Xi2,Yi2,GiXi1,Yi1,Xi2,Yi2,Gi:当 Gi≥1Gi≥1 时,表示 (Xi1,Yi1)(Xi1,Yi1) 单元与 (Xi2,Yi2)(Xi2,Yi2) 单元之间有一扇第 GiGi 类的门,当 Gi=0Gi=0 时,表示 (Xi1,Yi1)(Xi1,Yi1) 单元与 (Xi2,Yi2)(Xi2,Yi2) 单元之间有一面不可逾越的墙。

接下来一行,包含一个整数 SS,表示迷宫中存放的钥匙的总数。

接下来 SS 行,每行包含三个整数 Xi1,Yi1,QiXi1,Yi1,Qi,表示 (Xi1,Yi1)(Xi1,Yi1) 单元里存在一个能开启第 QiQi 类门的钥匙。

输出格式

输出麦克营救到大兵瑞恩的最短时间。

如果问题无解,则输出 -1。

数据范围

|Xi1−Xi2|+|Yi1−Yi2|=1|Xi1−Xi2|+|Yi1−Yi2|=1,
0≤Gi≤P0≤Gi≤P,
1≤Qi≤P1≤Qi≤P,
1≤N,M,P≤101≤N,M,P≤10,
1≤k≤1501≤k≤150

输入样例:

4 4 9
9
1 2 1 3 2
1 2 2 2 0
2 1 2 2 0
2 1 3 1 0 
2 3 3 3 0
2 4 3 4 1
3 2 3 3 0
3 3 4 3 0
4 3 4 4 0
2
2 1 2 
4 2 1

输出样例:

14

样例解释:

迷宫如下所示:

很明显,在普通的最短路层次上面,发生了转移的限制

比如,只有拿到钥匙才能打开门 。

所以要增加状态,

对st[],和dis[]

就要一起增加一维度

变成 st[][],和dis[][]
 

 

其次:

队列里面存的元素,要包含状态state (因为队列里面存放最基本的就是 st[][]和dis[][]的所有维度, 但是像迪杰除了dis的所有下标还会多一个 dis[]的值)

 

 

可以这样想:

之前dis[]/st[]只有一个维度的时候,也就只有一个转移(点与点的转移) 

现在dis[][]/st[][]有两个维度,就会有两个转移 (除了点与点的转移,还有状态与状态的转移)

我们之前所学的 枚举四个方向的,那是单纯的 点与点之间的转移

而本题,会有两个转移,一个是点维度,一个是状态维度

 1.第一个转移:第二维状态的转移

2.第二个转移:第一维点与点之间的转移

小结多状态问题:

1.deque队列里面存的元素会相应多状态

2.转移过程也会随着状态的增多而增多  (比如上面两个转移)

 

 

 

 

 

输入没有明显边权,那么边权如何产生?

本题属于不明显的边权

我们把边权分析出来的原因是:点与点之间的关系。 

点与点之间如果是门:则边权为1

点与点之间如果是墙:则没有边

点与点之间如果不是门也不是墙:则边权为0

注意:

该边权不能作为判定,加入双端队列队首还是队尾的条件

只能作为两个相邻点之间的关系。

真正的边权还要看 dis[]关于时间t的转移,

如果是+1的,那么就插入队首

否则插入队尾

做状态压缩的题目:

位运算的把握很重要,

首先我们要知道:

1.左移<<和右移>>是优先级最low的

 

两个操作:①存入状态 ②在状态里面查找

 2.搞清楚在状态压缩里面:什么时候要左移和右移?与怎么加入状态中?

首先先搞清楚  原始编号原始编号进行左移后的值 的区别

我们如果要把 “编号” 加入到状态里面,

只能把 1<<编号 加进去 (也就是原始编号经过处理的左移 

我假装给它一个 专有名词 :“处理后的原始编号”

而加进去有两种方式:

①state + (1<<编号)      加

②state|(1<<编号)         或

例如一下:key[]里面干脆不存原始编号了,就直接存处理过的编号:

 

 3.搞清楚如何检验在一个状态中,某个编号表示的事物存不存在?

ans:     state>>编号&1 

 

小结

对2,3可以发现 一个规律:

存入状态的时候 编号-1了,也就是 : 1<<id-1

那么从状态中查找的时候  编号也要-1,也就是 : t.y>>w[i]-1

//注释版代码
#include <bits/stdc++.h>
#define x first
#define y second 
using namespace std;
typedef pair<int,int> PII;
/*
钥匙有十把,
1.如果对钥匙开状态大小的话应该是P=1<<10

2.对二维的点使用一维的编号,应该是M=N*N

3.计算所有的边数E :
有竖着的边,也有横着的边
n*(n-1)*2(竖着横着) *2(正向反向) 
差不多是: 360
所以E开400
*/
const int N=11,M=N*N,P=1<<11,E=400;
int n,m,p;
int k; //表示墙和门的数量
/*
开边权w[]的原因,虽然题目中没有像以往的套路一样
输入就给我们边权
这里的边权隐藏在相邻点的关系中

边权为1:表示需要钥匙才能通过
边权为0:表示可以直接通过
*/
int e[E],ne[E],w[E],h[M],idx;
int g[N][N];  //存二维点的一维下标
int key[M];


int dis[M][P];  //第一维是点的一维下标,第二维是状态
bool st[M][P];


set<PII> edges; 
/*
开set的原因,由于我们整个图的所有点与相邻
的点之间都要存边
而输入只会告诉我们部分的边 (门、墙这一类两个点之间的关系)
所以我们为了后续补上其他边,
就要有个能判定是否被存的


但是如果是墙的话,就直接塞入set,假装建了边
实际没有建边
*/


void add(int a,int b,int c)
{
    e[idx]=b;
    ne[idx]=h[a];
    w[idx]=c;
    h[a]=idx++;
}

void build()
{
    int dx[4]={-1,0,1,0};
    int dy[4]={0,1,0,-1};
    
    for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
    {
        for(int u=0;u<4;u++)
        {
            int x=i+dx[u],y=j+dy[u];
            if(!x||x>n||!y||y>m) //出界
            {
                continue;
            }
            
            int a=g[i][j],b=g[x][y];  //把当前边的的左右端点编号提取出来
            if(!edges.count({a,b})) //判断一下有没有在set里
            {
                add(a,b,0);  //0的边权含义是不需要钥匙直接可以通过
            }
        }
    }
}

int bfs()
{
    memset(dis,0x3f,sizeof dis);
    dis[1][0]=0; //当前还没有钥匙,所以第二维是0
    
    deque<PII> q;
    q.push_back({1,0});
    
    while(q.size())
    {
        PII t=q.front();  //第一维是点的编号,第二维是state
        q.pop_front();
        
        if(st[t.x][t.y]) continue;
        st[t.x][t.y]=true;
        
        
        if(t.x==n*m)
        {
            return dis[t.x][t.y];
        }
        
        //第一个转移
        if(key[t.x])  //如果当前点有钥匙
        {
            int state=t.y|key[t.x];  //第二维新的状态
            if(dis[t.x][state]>dis[t.x][t.y])
            {
                dis[t.x][state]=dis[t.x][t.y];
                q.push_front({t.x,state});
            }
        }
        
        //第二个转移
        for(int i=h[t.x];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(w[i]&&!(t.y>>(w[i]-1)&1)) continue;
            /*如果当前边权是1(有门)
            但是当前位置在状态中发现没有钥匙,
            那么就要continue 掉
            
            注意w[i]-1的原因是key[t]|=1<<id-1;  存入状态-1了,那么在状态中找也要把编号-1
            */
            
            //剩下的情况要么是w[]为0直接通行,要么就是w[]为1但是有钥匙,总之dis[][]更新起来要+1
            //此处把边权为1的加入对尾好像和w[]为0和1冲突了,毕竟意义上
            //所以以后就把w[]当做两点间的关系吧
            //真正的边权就看 dis是否是更新了加一的
            if(dis[j][t.y]>dis[t.x][t.y]+1)
            {
                dis[j][t.y]=dis[t.x][t.y]+1;
                q.push_back({j,t.y});
            }
        }
        
    }
    
    return -1;
}


int main()
{
    cin>>n>>m>>p>>k;
    
    /*读入k条边之前,把二维点都编上号
      打表
    */
    for(int i=1,t=1;i<=n;i++)
    for(int j=1;j<=m;j++)
    {
        g[i][j]=t++;
    }
    
    memset(h,-1,sizeof h);
    
    
    while(k--)
    {
        int x1,y1,x2,y2,c;  //(x1,y1) (x2,y2) 和一个类型c
        cin>>x1>>y1>>x2>>y2>>c;
        int a=g[x1][y1],b=g[x2][y2];
        edges.insert({a,b}),edges.insert({b,a});
        
        if(c)  //如果不是墙
        {
            add(a,b,c);
            add(b,a,c);
        }
    }
    
    build(); //把其他未建立的边建起来
    
    
    int s;  //钥匙的数量
    cin>>s;
    while(s--)
    {
        int x,y,id;
        cin>>x>>y>>id;   //表示x,y处有钥匙
        
      //  int t=g[x][y]; //点的一维下标
        
       // key[t]+=1<<id-1;
        key[g[x][y]]|=1<<(id-1); //"-"的优先级 大于 "<<",这么做-1是为从0开始
        
    }
    
    cout<<bfs()<<endl;
    
    return 0;
    
}
//无注释版代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <deque>
#include <set>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 11, M = 360, P = 1 << 10;

int n, m, k, p;
int h[N * N], e[M], w[M], ne[M], idx;
int g[N][N], key[N * N];
int dist[N * N][P];
bool st[N * N][P];

set<PII> edges;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void build()
{
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            for (int u = 0; u < 4; u ++ )
            {
                int x = i + dx[u], y = j + dy[u];
                if (!x || x > n || !y || y > m) continue;
                int a = g[i][j], b = g[x][y];
                if (!edges.count({a, b})) add(a, b, 0);
            }
}

int bfs()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1][0] = 0;

    deque<PII> q;
    q.push_back({1, 0});

    while (q.size())
    {
        PII t = q.front();
        q.pop_front();

        if (st[t.x][t.y]) continue;
        st[t.x][t.y] = true;

        if (t.x == n * m) return dist[t.x][t.y];

        if (key[t.x])
        {
            int state = t.y | key[t.x];
            if (dist[t.x][state] > dist[t.x][t.y])
            {
                dist[t.x][state] = dist[t.x][t.y];
                q.push_front({t.x, state});
            }
        }

        for (int i = h[t.x]; ~i; i = ne[i])
        {
            int j = e[i];
            if (w[i] && !(t.y >> w[i] - 1 & 1)) continue;   // 有门并且没有钥匙
            if (dist[j][t.y] > dist[t.x][t.y] + 1)
            {
                dist[j][t.y] = dist[t.x][t.y] + 1;
                q.push_back({j, t.y});
            }
        }
    }

    return -1;
}

int main()
{
    cin >> n >> m >> p >> k;

    for (int i = 1, t = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            g[i][j] = t ++ ;

    memset(h, -1, sizeof h);
    while (k -- )
    {
        int x1, y1, x2, y2, c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        int a = g[x1][y1], b = g[x2][y2];

        edges.insert({a, b}), edges.insert({b, a});
        if (c) add(a, b, c), add(b, a, c);
    }

    build();

    int s;
    cin >> s;
    while (s -- )
    {
        int x, y, c;
        cin >> x >> y >> c;
        key[g[x][y]] |= 1 << c - 1;
    }

    cout << bfs() << endl;

    return 0;
}


 

 

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 SQL Server 中,使用 INSERT 语句可以向表中添加新的行。如果想要加速 INSERT 操作,可以考虑使用并行插入,以便在多个处理器上同时执行插入操作。以下是实现并行插入的步骤: 1. 首先,将目标表设置为允许并行插入。可以使用以下命令来查看当前是否启用了并行插入: ```sql SELECT is_parallel_insert_on FROM sys.databases WHERE name = 'database_name'; ``` 如果返回结果为 1,则表示启用了并行插入。如果返回结果为 0,则需要启用并行插入。 启用并行插入的命令如下: ```sql ALTER DATABASE database_name SET PARALLEL_INSERT ON; ``` 2. 然后,在 INSERT 语句中使用 TABLOCK hint,以便在执行插入操作时锁定整个表。这样可以确保多个线程不会同时尝试插入同一行,从而提高并行插入的效率。 INSERT 语句如下: ```sql INSERT INTO table_name WITH (TABLOCK) (column1, column2, ..., columnN) VALUES (value1, value2, ..., valueN); ``` 3. 最后,使用多个连接同时执行 INSERT 语句。可以使用多个线程或使用 BULK INSERT 命令来实现。 使用多个线程执行 INSERT 语句的示例代码如下: ```sql -- 创建线程表 CREATE TABLE thread_table (thread_id INT PRIMARY KEY); -- 插入线程数据 INSERT INTO thread_table VALUES (1), (2), (3), (4); -- 使用多个线程执行 INSERT 语句 DECLARE @thread_id INT; DECLARE @sql NVARCHAR(MAX); DECLARE thread_cursor CURSOR FOR SELECT thread_id FROM thread_table; OPEN thread_cursor; FETCH NEXT FROM thread_cursor INTO @thread_id; WHILE @@FETCH_STATUS = 0 BEGIN SET @sql = 'INSERT INTO table_name WITH (TABLOCK) (column1, column2, ..., columnN) VALUES (value1, value2, ..., valueN);'; EXECUTE sp_executesql @sql; FETCH NEXT FROM thread_cursor INTO @thread_id; END CLOSE thread_cursor; DEALLOCATE thread_cursor; ``` 使用 BULK INSERT 命令执行 INSERT 操作的示例代码如下: ```sql BULK INSERT table_name FROM 'data_file' WITH (TABLOCK); ``` 需要注意的是,并行插入操作可能会占用更多的系统资源,在高并发环境下可能会导致性能问题。因此,在使用并行插入时需要权衡性能和系统资源的消耗。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值