分类 理论学习 下的文章

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>

在与联盟的战斗中屡战屡败后,帝国撤退到了最后一个据点。依靠其强大的防御系统,帝国击退了联盟的六波猛烈进攻。 经过几天的苦思冥想,联盟将军亚瑟终于注意到帝国防御系统唯一的弱点就是能源供应。
该系统由 N 个核电站供应能源,其中任何一个被摧毁都会使防御系统失效。
将军派出了 N 个特工进入据点之中,打算对能源站展开一次突袭。不幸的是,由于受到了帝国空军的袭击,他们未能降落在预期位置。
作为一名经验丰富的将军,亚瑟很快意识到他需要重新安排突袭计划。
他现在最想知道的事情就是哪个特工距离其中任意一个发电站的距离最短。
你能帮他算出来这最短的距离是多少吗?

输入格式:

输入中包含多组测试用例。
第一行输入整数 T,代表测试用例的数量。
对于每个测试用例,第一行输入整数 N(1≤N≤100000 )。
接下来 N 行,每行输入两个整数 X(0≤X) 和 Y(Y≤1000000000),代表每个核电站的位置的 X,Y 坐标。
再接下来 N 行,每行输入两个整数 X 和 Y,代表每名特工的位置的 X,Y 坐标。

输出格式:

每个测试用例,输出一个最短距离值,结果保留三位小数。
每个输出结果占一行。

输入样例:

在这里给出一组输入。例如:

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

输出样例:

在这里给出相应的输出。例如:

1.414
0.000
### 源代码
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const double INF=1e10;
const int N=100005;
 
int n;
struct node{
	double x,y;
	int flag;
	bool operator < (const node &a) const{
		return x<a.x;
	} 
}opt[N<<1],temp[N<<1];//在一段实现功能的程序中,如果要使用temp,就要先对要使用的部分进行赋值

double dis(node a,node b){//返回两点之间的距离 
	if(a.flag==b.flag) return INF;
	return sqrt(pow(abs(a.x-b.x),2)+pow(abs(a.y-b.y),2));
}

double solve(int l,int r){// 返回排序后下标l到r这个区间中最小点距
	//如果下标区间只有这一个点 返回最大值  
	if(l>=r) return INF;
	
	//二分递归,求两个左右区间的最小点距,并且将小的赋给ans存储 
	int mid=(l+r)>>1;
	double mid_x=opt[mid].x;
	double ans=min(solve(l,mid),solve(mid+1,r));
	
	//二分排序的合并过程  
	int left=l,right=mid+1,t=l;
	while(left<=mid && right<=r){//下标区间内所有点存于temp的相同下标区间
		if(opt[left].y<=opt[right].y) temp[t++]=opt[left++];
		else temp[t++]=opt[right++];
	}//先执行只有两个点的排序,成功比较大小;后面更多点的排序建立在其基础上:两个已排序的数列插值合并还是已排序的。
	while(left<=mid) temp[t++]=opt[left++];//这两个循环只有一个能进行
	while(right<=r) temp[t++]=opt[right++];
	for(int i=l;i<=r;i++){
		opt[i]=temp[i];
	}
	
	//从左到右将opt中的在mid_x-ans至mid_x+ans的点存进temp
	//这样temp中的点既是按y排序的,又是在mid_x-ans至mid_x+ans区间里的 
	int k=0;
	for(int i=l;i<=r;i++){
		if(opt[i].x>=mid_x-ans&&opt[i].x<=mid_x+ans){
			temp[k++]=opt[i];
		}
	}
	
	//从有序的temp数组中 找到每个点y坐标上面不超过ans的点的点距离求出
	//并且更新答案  
	for(int i=0;i<k;i++){
		for(int j=i-1;j>=0&&temp[i].y-temp[j].y<=ans;j--){
			ans=min(ans,dis(temp[i],temp[j]));
		}
	}
	
	//返回这个l到r下标的最小点距 
	return ans;
}

int main()
{
	int t;
	cin>>t;
	while(t--){
		cin>>n;
		for(int i=0;i<n;i++) {
			cin>>opt[i].x>>opt[i].y;
			opt[i].flag=0;
		}
		for(int i=n;i<n<<1;i++) {
			cin>>opt[i].x>>opt[i].y;
			opt[i].flag=1;
		}
		sort(opt,opt+(n<<1));
		printf("%.3f\n",solve(0,(n<<1)-1));
	}
	return 0;
} 

### 题解

分治、穷举、贪心?

由于题目相比普通的最近点对问题,要求出两点集间的最短距离,为避免分类可以假设不同点集的点是一个点集内距离为INF的点。距离函数dis()。

先把点按x坐标排序。在某下标区间[l,r]的solve函数也实现了该下标区间内的点转换成按y排序再储存到原处。

区间按下标区间中点mid分成两块,最近距离为\(\min\{d_左,d_右,d_{左右}\}\)。

求两边点间最短距离的贪心算法,可以只取mid点x坐标旁边的部分点满足\(mid.x-ans \leq x \leq mid.x+ans,\qquad ans=\min\{d_左,d_右\}\)。

用一般方法求出这部分点间的最小距离,就完成该步的操作,返回该下标区间内的最近距离。

递归solve得到答案。

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script><style type="text/css">em{font-size: large;font-weight: bold;}</style>

洛谷P2324 [SCOI2005] 骑士精神

题目在P2324 [SCOI2005] 骑士精神

初步分析

输入棋盘当前的状态,要求出从当前状态转移到目标状态所需要的最少变换次数。将每种状态作为无向图的一个节点,能通过走“马”字相互转换的两种状态对应图上的两个相邻节点。

此时问题就转变成求从某结点A到目标节点B的最短路径的长度。

棋盘的状态不同,即棋盘上黑白子的位置、空白的位置不同;棋盘不同状态的转换通过能移动到空白处的棋子移动(改变位置)完成。

算法选用

假设大师在这题会用IDA*。
  • 棋盘的状态非常多,而题目有15的路径长度限制,饱学之士将能使用启发式搜索进行剪枝。对于图中的任意点M,经过该点的路径长\(l=AM+MB \leq 15\)。从始点开始绘制路径到\(M\)时\(MA\)已经有值,则在搜索树上剪去\(MB>15-AM\)的结点。

    可以不严谨地证明当棋盘在M状态下有n个位置的状态不同时,最少需要n-1步来达到相同,计算两个状态间不同的个数为\(n = h(M,B)\) ,则在搜索树上剪去\(n-1+AM \geq 15\)的结点。这是启发式搜索的估值函数

  • 常用求最短路径的广度优先搜索由于不好储存节点状态(可能超内存)而不能实行,深度优先搜索由于连续两次搜索的状态相邻可以避免这一问题。为避免深度过大超时需要最大深度常量,在递归函数中能访问,与当前深度做判断是否剪枝

    一般使用此方法会让深度递增到最大值,每个深度都重新搜索,叫限制深度的(迭代加深)深度优先搜索

代码实现

  1. 创建一些全局量
  2. 这题的终点是个常量。走“马”也很常规。

    //最后目标
    int goal[5][5] = {
    {1,1,1,1,1},
    {0,1,1,1,1},
    {0,0,2,1,1},
    {0,0,0,0,1},
    {0,0,0,0,0}
    };
    //用于走马
    int dx[8] = { 1,1,2,2,-1,-1,-2,-2 };
    //用于走马
    int dy[8] = { 2,-2,1,-1,2,-2,1,-1 };

  3. 输入储存数据
  4. 没有什么特别,注意输入数据的字符中间没有空格。

    char c;
    for (int i = 0; i < 5; ++i)
    {
    for (int j = 0; j < 5; ++j)
    {
    cin >> c;
    if (c == '*') {
    blank[0] = i;
    blank[1] = j;
    chessboard[i][j] = 2;
    }
    else chessboard[i][j] = c - '0';
    }
    }//输入了棋盘

  5. “ID”部分
  6. 终止条件是深度depth、估值函数eval满足\(depth>maxdepth\)、\(eval+depth-1>maxdepth\)回溯;或\(eval=0\)跳出递归,将depth赋给能在main()中被访问的变量。

    这里选择当前节点的空白处位置x和y、棋子位置chessboard(放在了全局)、当前深度depth和本轮的最大深度maxdepth作为递归dfs()的参数。

    //储存棋盘
    int chessboard[5][5];
    //标记是否成功了
    bool succ = false;
    //限制深度的深搜
    void dfs(int depth,int x,int y, int maxdepth) {
    if (depth > maxdepth) {
    return;
    }//步数超过了限制的最大步数
    int count = eval();
    if (count == 0) {
    succ = true;
    return;
    }//达成目标
    if (depth + count - 1 > maxdepth)
    {
    return;
    }//未来不能满足条件了

    for (int i = 0; i lt; 8; ++i) {
    int a = x + dx[i], b = y + dy[i];
    if (0 <= a && a < 5 && 0 <= b && b < 5) {
    swap(chessboard[a][b], chessboard[x][y]);
    dfs(depth + 1, a, b, maxdepth);
    if (succ)return;
    swap(chessboard[a][b], chessboard[x][y]);//回溯
    }
    }
    }
    int main(){
    ...
    int ans = -1;
    for (int i = 1; i <= 15; ++i) {
    dfs(0, x, y, i);
    if (succ) {
    ans = i;
    break;
    }
    }
    cout << ans << endl;
    ...
    }

  7. 估值函数
  8. 根据前面的分析,估值函数只需要统计两个状态的不同处的个数并返回就可以。

    //估值函数
    int eval() {
    int ans = 0;
    for (int i = 0; i < 5; ++i) {
    for (int j = 0; j < 5; ++j) {
    if (chessboard[i][j] != goal[i][j]) {
    ans++;
    }
    }
    }
    return ans;
    }

最终代码

#include<iostream>

using namespace std;
//储存棋盘
int chessboard[5][5];
//最后目标
int goal[5][5] = {
{1,1,1,1,1},
{0,1,1,1,1},
{0,0,2,1,1},
{0,0,0,0,1},
{0,0,0,0,0}
};
//用于走马
int dx[8] = { 1,1,2,2,-1,-1,-2,-2 };
//用于走马
int dy[8] = { 2,-2,1,-1,2,-2,1,-1 };
//空白坐标
int blank[2];
//标记是否成功了
bool succ = false;

//估值函数
int eval() {
int ans = 0;
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 5; ++j) {
if (chessboard[i][j] != goal[i][j]) {
ans++;
}
}
}
return ans;
}

//限制深度的深搜
void dfs(int depth,int x,int y, int maxdepth) {
//if (maxdepth == 7)cout << depth << endl;
if (depth > maxdepth) {
return;
}//步数超过了限制的最大步数
int count = eval();
if (count == 0) {
succ = true;
return;
}//达成目标
if (depth + count - 1 > maxdepth)
{
return;
}//未来不能满足条件了

for (int i = 0; i < 8; ++i) {
int a = x + dx[i], b = y + dy[i];
if (0 <= a && a < 5 && 0 <= b && b < 5) {
swap(chessboard[a][b], chessboard[x][y]);
dfs(depth + 1, a, b, maxdepth);
if (succ)return;
swap(chessboard[a][b], chessboard[x][y]);//回溯
}
}
}

int main() {
int t; cin >> t;
while (t--)
{
succ = false;
char c;
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < 5; ++j)
{
cin >> c;
if (c == '*') {
blank[0] = i;
blank[1] = j;
chessboard[i][j] = 2;
}
else chessboard[i][j] = c - '0';
}
}//输入了棋盘

int ans = -1;
for (int i = 1; i <= 15; ++i) {
dfs(0, blank[0], blank[1], i);
if (succ) {
ans = i;
break;
}
}
cout << ans << endl;
}
}

附加内容

对于没见过迭代加深DFS的菜的人,可以通过构造一个小的数据结构来储存状态,避免超出内存。使用广度优先搜索和A*。

广度优先搜索中,对每个结点的搜索(直接写在了main()中)选用描述当前棋盘状态的board、当前深度depth作为隐式的参数,最大深度15可以看作常量参数。由于难以回溯而采取用队列queue<pack>储存信息的方式。

添加该状态的前一个状态的空白处坐标在pack结构内,可以减去重复枝;应该也可以用unordered_map来储存搜索过的所有状态来减少重复。代码使用了priority_queue来提速。

#include<iostream>
#include<bitset>
#include<queue>
#include<chrono>

using namespace std;
typedef bitset<25> bs;

//用于走马
int dx[8] = { 1,1,2,2,-1,-1,-2,-2 };
//用于走马
int dy[8] = { 2,-2,1,-1,2,-2,1,-1 };

//记录棋盘状态
struct chessboard {
bs pieces;
int blank;
//空白位置的坐标
const pair cdntBlank() const {
return make_pair(blank / 5, blank % 5);
}
//将(x,y)转换成pos
static int castPos(const pair& p) {
if (0 <= p.first && p.first < 5 && 0 <= p.second && p.second < 5) {
return p.first * 5 + p.second;
}
else return -1;
}
};
const chessboard goal = { 549855 ,12 };

//返回移动后的棋盘状态
chessboard moveReturn(const chessboard& c, int pos) {
bs temp = c.pieces;
temp[c.blank] = c.pieces[pos];
temp[pos] = 0;
return chessboard{ move(temp) ,pos };
}

//估值函数
int evaluateDistance(const chessboard& crt_board, const chessboard& goal) {
int ans = 0;
for (int i = 0; i < 25; ++i) {
ans = (crt_board.pieces ^ goal.pieces).count();
}
if (!crt_board.pieces[goal.blank])ans++;
if (!goal.pieces[crt_board.blank])ans++;
if (goal.blank == crt_board.blank)ans -= 2;
return ans;
}

//用于队列的类
struct pack
{
chessboard board;
int last_blank;
int depth;
int eval;
class less {
public:
int operator()(const pack& a, const pack& b) {
return a.depth + a.eval > b.depth + b.eval;
}
};
};

//queue bfs_queue;

int main() {
int t; cin >> t;
const int MAXDEPTH = 15;
while (t--)
{
chessboard board{ 0,0 };
bs temppieces;
for (int i = 0; i < 25; ++i) {
static char c;
cin >> c;
if (c == '1')temppieces[i] = 1;
else if (c == '*')board.blank = i;
//else temppieces[i] = 0;
}
board.pieces = move(temppieces);//输入棋盘

//bfs_queue = queue();
int ans = -1;
priority_queue,pack::less> bfs_queue;
bfs_queue.push({ board,-1,0,evaluateDistance(board,goal) });
int pos;
do
{
pack front = bfs_queue.top();
bfs_queue.pop();

if (front.eval == 0) {
ans = front.depth;
break;
}
else if (front.depth >= MAXDEPTH) {
continue;
}
if (front.depth + front.eval - 1 > MAXDEPTH) {
continue;
}

auto x_y = front.board.cdntBlank();
int cx_y = chessboard::castPos(x_y);
for (int i = 0; i < 8; ++i) {
pos = chessboard::castPos(make_pair(x_y.first + dx[i], x_y.second + dy[i]));
if (pos == front.last_blank)continue;
if (pos != -1) {
chessboard newboard = move(moveReturn(front.board, pos));
bfs_queue.push({ newboard,
cx_y,
front.depth + 1,
evaluateDistance(newboard,goal) });
}
}
} while (!bfs_queue.empty());
cout << ans << endl;
}
}

正方形 - 计蒜客

2024-07-09T12:29:56.png

题意分析:

搜索4条边,使得4条边满足相等而且等于sum/4,即数组所有元素一起能构成一个正方形

初代算法:

dfs(int id, int l1,int l2,int l3,int l4)
{
    dfs(id+1,l1+p[id],l2,l3,l4);
    dfs(id+1,l1,l2+p[id],l3,l4);
    dfs(id+1,l1,l2,l3+p[id],l4);
    dfs(id+1,l1,l2,l3,l4+p[id]);
}

对4条边进行搜索 会超时

分析:

每条边都是等效的,因此重复状态很多

不是很理解ac代码的k的作用.

#include <stdio.h>
#define MAXN 25
long long p[MAXN], n;
long long sum = 0;
long long a = 0;

int ok = 0;
long long vis[MAXN];
// x表示当前正在搜索第x(0,1,2)条边
// sum 表示当前搜索的边的长度
// k表示//避免重复状况(?)
// 即 若当前dfs的k仍从1开始,
// 遇到一个下标介于1和k之间的下标(记为j),
// 并且发现vis[j]=0,
// 那么其实已经说明j被这层dfs前面的dfs搜索过,
// 而且还不是正确答案状态,
// 可以省略这些状态,
// 所以每层k都要进行+1
void dfs(int sum,int k,int x)
{
    if(ok)
    {
        return;
    }
    if(x==3)
    {
        ok=1;
        return;
    }
    if(sum>a)
    {
        return;
    }
    if(sum==a)
    {
        dfs(0,0,x+1);
    }
    for(int i =k;i<=n;i++)
    {
        if(!vis[i])
        {
            vis[i]=1;
            dfs(sum+p[i],i+1,x);
            vis[i]=0;
        }
    }
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &p[i]);
        sum += p[i];
    }
    if(sum%4!=0)
    {
        printf("No");
        return 0; 
    }
    a = sum / 4;

    for(int i = 1;i<=n;i++)
    {
        if(p[i]>a)
        {
            printf("No");
            return 0;
        }
    }
    dfs(0,1,0);
    if (ok)
    {
        printf("Yes");
    }
    else
    {
        printf("No");
    }
}

引爆炸弹 - 来自计蒜客小学期

题目:

2024-07-08T04:52:04.png 2024-07-08T04:52:17.png

题意分析:

1.引爆一个炸弹,则属于该行该列的炸弹都会被引爆,结果被引爆的每个炸弹也递归引爆剩余的炸弹

2.最少需要手动引爆多少个,我们可以把会连续引爆的炸弹们归为同一个集合里面,这个集合里的任何一个炸弹被引爆,整个集合都会被引爆。因此这个集合的所有炸弹是等效的

3.那么我们只需要首先引爆某个炸弹,然后对集合进行标记引爆,搜索染色直到所有炸弹都炸了。

思路:

  1. 统计地图中的所有炸弹和炸弹坐标

  2. 先引爆某个炸弹

  3. 对这个炸弹所在的集合进行染色

  4. 深搜,即枚举剩下的炸弹,找到剩下的集合 如果找到了 则手动的步数+1

注意:

  1. 只需要进行染色即可 不需要recolor

  2. dfs起点只需要随意一个炸弹即可

  3. 地图没有存储的必要

代码:


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAXN 666
char map[MAXN][MAXN];
int n, m;
int ans = MAXN * MAXN;
struct BOMB
{
    int x, y;
} bombs[MAXN * MAXN];
struct VISB
{
    char visb;
    int father;
} visb[MAXN * MAXN];
int bombcnt;
int exploded;
int queue[MAXN*MAXN],tot;
inline int same_block(int i, int j)
{
    if ((bombs[i].x == bombs[j].x) || (bombs[i].y == bombs[j].y))
    {
        return 1;
    }
    return 0;
}
void color_dfs(int b)
{
    queue[++tot] = b;
    while(tot!=0)
    {
        int x = queue[tot--];
        for(int i = 0;i<bombcnt;i++)
        {
            if(same_block(x,i)&&!visb[i].visb)
            {
                exploded++;
                if(exploded==bombcnt)
                {
                    return;
                }
                visb[i].visb = 1;
                visb[i].father = x;
                queue[++tot] = i;
            }
        }
    }
}
void decolor_dfs(int b)
{
    queue[++tot] = b;
    while(tot!=0)
    {
        int x = queue[tot--];
        for(int i =0;i<bombcnt;i++)
        {
            if(same_block(x,i)&&visb[i].visb&&visb[i].father==x)
            {
                visb[i].visb = 0;
                exploded--;
                visb[i].father = -1;
                queue[++tot] = i;
            }
        }
    }
}

int dfs(int b, int cnt)
{
    if (cnt > ans)
    {
        return 0;
    }
    color_dfs(b);
    if (exploded == bombcnt)
    {
        printf("%d",cnt);
        return 1;
    }
    for (int i = 0; i < bombcnt; i++)
    {
        if (!visb[i].visb)
        {
            if(dfs(i, cnt + 1))
            {
                return 1;
            }
        }
    }
    //decolor_dfs(b);
    return 0;
}
int main()
{
    freopen("E:\\Downloads\\4.txt","r",stdin);
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++)
    {
        scanf("%s", map[i]);
    }
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < m; j++)
        {
            if (map[i][j] == '1')
            {
                bombs[bombcnt].x = i;
                bombs[bombcnt].y = j;
                bombcnt++;
            }
        }
    }

    dfs(rand()%bombcnt, 1);

}