Algorithm design and analysis -- backtracking method

Posted by focus310 on Sun, 12 Sep 2021 05:37:48 +0200

Algorithm design and analysis -- backtracking method ©

1, Backtracking method

1. Definition

In the solution space tree containing all solutions of the problem, according to the depth first search strategy, search the solution space tree from the root node (start node).

When the backtracking method searches the solution space, two strategies are usually adopted to avoid invalid search and improve the search efficiency of backtracking:

The constraint function is used to prune the subtree that does not meet the constraint at the extension node;
Cut out the subtree that can not get the problem solution or optimal solution with the bound function.

These two kinds of functions are collectively referred to as pruning functions.

Summary:

Backtracking = depth first search + pruning

2. General steps of backtracking problem solving

① Determine the solution space tree of the problem, and the solution space tree of the problem should contain at least one (optimal) solution of the problem.

② Determine the extension rules of the node.

③ The solution space tree is searched by depth first, and the pruning function can be used to avoid invalid search.

3. Algorithm framework of backtracking method

1. Non recursive backtracking framework

int x[n];				//x storage solution vector, global variable
void backtrack(int n)			//Non recursive framework
{  int i=1;				//The root node level is 1
   while (i>=1)			//Not yet back to the beginning
   {  if(ExistSubNode(t)) 		//The current node has child nodes
      {  for (j=Lower bound;j<=upper bound;j++)	//For subset trees, j=0 to 1 cycles
         {  x[i]Take a possible value;
            if (constraint(i) && bound(i)) 
					//x[i] satisfies constraints or boundary functions
            {  if (x Is a feasible solution)
		   output x;
               else	i++;		//Go to the next level
	     }
         }
      }
      else  i--;			//Backtracking: if there is no child node, return to the previous layer
   }
}

2. Recursive algorithm framework

(1) The solution space is a subset tree
int x[n];			   //x storage solution vector, global variable
void backtrack(int i)		   //Recursive framework for solving subset tree
{  if(i>n)			   //Search the leaf node and output a feasible solution
      Output results;
   else
   {  for (j=Lower bound;j<=upper bound;j++)   //Enumerate i all possible paths with j
      {  x[i]=j;		   //Generate a possible solution component
         ...			   //Other operations
         if (constraint(i) && bound(i))
            backtrack(i+1);	   //Satisfy the constraints and the bound function, and continue to the next level
      }
   }
}

[example] there is an array a with n integers. All elements are different. Design an algorithm to find all its subsets (power set).

For example, a[]={1, 2, 3}, all subsets are: {}, {3}, {2}, {2, 3}, {1, 3}, {1, 2}, {1, 2, 3} (independent of output order).

analysis:

Obviously, the solution space of this problem is a subset tree, and each element has only two extensions, either or not.

Depth first search is adopted. The solution vector is * * x [], x[i]=0 means not selecting a[i], and x[i]=1 means selecting a[i].

Scan array a with i, that is, the initial state of the problem is (i=0, the elements of X are 0), and the target state is (i=n, X is a solution). Two states can be extended from state (i, x):

No choice a[i]element  The next status is( i+1,x[i]=0). 
choice a[i]element  The next status is( i+1,x[i]=1). 

The code is as follows:

#include <stdio.h>
#include <string.h>
#define MAXN 100
void dispasolution(int a[],int n,int x[])	//Output a solution
{
	printf("   {");
	for (int i=0;i<n;i++)
		if (x[i]==1)
			printf("%d",a[i]);
	printf("}");
}
void dfs(int a[],int n,int i,int x[])	//Backtracking algorithm
{
	if (i>=n)
		dispasolution(a,n,x);
	else
	{
		x[i]=0;
		dfs(a,n,i+1,x);			//Do not select a[i]
		x[i]=1;
		dfs(a,n,i+1,x);			//Select a[i]
	}
}
int main()
{
	int a[]={1,2,3};				//s[0..n-1] is the given string and is set as a global variable
	int n=sizeof(a)/sizeof(a[0]);
	int x[MAXN];					//Solution vector
	memset(x,0,sizeof(x));			//Solution vector initialization
	printf("Solution results\n");
	dfs(a,n,0,x);
	printf("\n");
}
(2) The solution space is a permutation tree
int x[n];			//x stores the solution vector and initializes it
void backtrack(int i)		//Recursive framework for solving permutation tree
{  if(i>n)			//Search the leaf node and output a feasible solution
	Output results;
   else
   {  for (j=i;j<=n;j++)	//Enumerate i all possible paths with j
      {  ...			//Operation of node selection x[j] of layer i
         swap(x[i],x[j]);	//In order to ensure that each element in the arrangement is different, it is realized by exchange
         if (constraint(i) && bound(i))
	     backtrack(i+1);	//Meet the constraint conditions and limit function, and enter the next layer
         swap(x[i],x[j]);	//Restore state
         ...			//Recovery operation of node selection x[j] in layer i
      }
   }
}

[example] there is an array a with n integers. All elements are different. Find the full arrangement of all elements.

For example, a[]={1, 2, 3}, the result is (1, 2, 3), (1, 3, 2), (2, 3, 1), (2, 1, 3), (3, 1, 2), (3, 2, 1).

The code is as follows:

//Algorithm of example 5.5
#include <stdio.h>
void swap(int &x,int &y)			//Swap x, y
{	int tmp=x;
	x=y; y=tmp;
}
void dispasolution(int a[],int n)	//Output a solution
{
	printf("  (");
	for (int i=0;i<n-1;i++)
		printf("%d,",a[i]);
	printf("%d)",a[n-1]);
}
void dfs(int a[],int n,int i)		//Find the total arrangement of a[0..n-1]
{
	if (i>=n)							//Recursive exit
		dispasolution(a,n);
	else
	{	for (int j=i;j<n;j++)
		{	swap(a[i],a[j]);			//Exchange a[i] and a[j]
			dfs(a,n,i+1);
			swap(a[i],a[j]);			//Exchange a[i] and a[j]: recovery
		}
	}
}
int main()
{
	int a[]={1,2,3,4};
	int n=sizeof(a)/sizeof(a[0]);
	printf("a Full Permutation of\n");
	dfs(a,n,0);
	printf("\n");
}

4. Similarities and differences between backtracking and depth first traversal

1. Similarities between the two:

The implementation of backtracking method also follows the principle of depth first, that is, exploring step by step, rather than searching from near to far as breadth first traversal.

2. Differences between the two:

(1) The access order is different: the purpose of depth first traversal is "traversal", which is disordered in nature, while the purpose of backtracking method is "solution process", which is orderly in nature.

(2) Different access times: depth first traversal no longer accesses the accessed vertices, and all vertices are accessed only once. In the backtracking method, the accessed vertices may be accessed again.

(3) Pruning is different: depth first traversal does not include pruning, while many backtracking algorithms use pruning conditions to cut unnecessary branches to improve efficiency.

5. Solving the loading problem

[problem description] there are n containers to be loaded on a ship with a load capacity of W, in which the weight of container i (1 ≤ i ≤ n) is wi. Regardless of the volume limit of containers, several loading ships with a weight less than or equal to W and as large as possible should be selected from these containers.

For example, when n=5, W=10, w={5, 2, 6, 4, 3}, the best loading scheme is (1, 1, 0, 1) or (0, 0, 1, 1, 0), maxw=10.

[problem solving] the backtracking method with pruning is used to solve the problem. The expression of the problem is as follows:

int w[]={0, 5, 2, 6, 4, 3}; / / weight of each container without subscript 0

int n=5,W=10;

The solution results are shown as follows:

int maxw=0; / / total weight of the optimal solution

int x[MAXN]; / / store the optimal solution vector

Design the above data as global variables.

The solution algorithm is as follows:

void dfs(int i,int tw,int rw,int op[])

Where parameter i represents the container i under consideration, tw represents the sum of the selected container weights, rw represents the sum of the remaining container weights (initially the sum of all container weights), and op represents a solution, that is, corresponding to a loading scheme.

The code is as follows:

#include <stdio.h>
#include <string.h>
#define MAXN 20 						// Maximum number of containers

int w[]={0,5,2,6,4,3};				//Weight of each container without subscript 0
int	n=5,W=10;
int maxw;							//Total weight of storing optimal solution
int x[MAXN];						//Store optimal solution vector
int minnum=999999;					//The initial value of the number of containers storing the optimal solution is the maximum
void dfs(int num,int tw,int rw,int op[],int i) //Consider the ith container
{
	if (i>n)						//Find a leaf node
	{
		if (tw==W && num<minnum)
		{	maxw=tw;				//Find a better solution that satisfies the conditions and save it
			minnum=num;
			for (int j=1;j<=n;j++)	//Copy optimal solution
				x[j]=op[j];
		}
	}
	else						//Not all containers have been found
	{	op[i]=1;				//Select the ith container
		if (tw+w[i]<=W)			//Left child node pruning: load containers that meet the conditions
			dfs(num+1,tw+w[i],rw-w[i],op,i+1);
		op[i]=0;				//Do not select the ith container, backtracking
		if (tw+rw>W)			//Right child node pruning
			dfs(num,tw,rw-w[i],op,i+1);
	}
}
void dispasolution(int n)		//Output a solution
{
	for (int i=1;i<=n;i++)
		if (x[i]==1)
			printf("  Select section%d Containers\n",i);
	printf("Total weight=%d\n",maxw);
}
int main()
{
	int op[MAXN];				//Store temporary solution
	memset(op,0,sizeof(op));
	int rw=0;
	for (int i=1;i<=n;i++)
		rw+=w[i];
	dfs(0,0,rw,op,1);
	printf("Optimal scheme\n");
	dispasolution(n);
}

2, Backtracking experiment

1. Experiment 1 solved the complex loading problem

[problem description] there are a batch of n containers to be loaded with two ships with load capacity of c1 and c2 respectively, in which the weight of container i is wi and w1+w2 +... + wn ≤ c1+c2.

The loading problem requires determining whether there is a reasonable loading scheme to load these containers on these two ships. If so, find a loading scheme.

For example:

  • When n=3, c1=c2=50, w={10, 40, 40}, containers 1 and 2 can be loaded on the first ship and container 3 can be loaded on the second ship.
  • If n=3, c1=c2=50, w={20, 40, 40}, the three containers cannot be loaded on the ship.

[problem solving] if a given complex loading problem has a solution, a loading scheme can be obtained in the following way:

First fill the first ship as full as possible, and then load the remaining containers on the second ship.

  • Its correctness can be proved by counter evidence.
  • If you can't get a loading scheme in this way, it means that the complex loading problem has no solution!

Algorithm idea (input w1, w2,..., wn, c1, c2):

(1) Load as many containers as possible on the first ship to obtain the solution vector x.

(2) Accumulate the remaining container weight sum after the first ship is loaded.

(3) If sum < = C2, it indicates that the second ship can be loaded, and returns true; Otherwise, it means that the second ship cannot be loaded, and false is returned.

The code is as follows:

#include <stdio.h>
#include <string.h>
#define MAXN 20 						// Maximum number of containers

int w[]={0,10,40,40};				//Weight of each container without subscript 0
int	n=3;
int c1=50,c2=50;
int maxw=0;							//The total weight of the optimal solution of the first ship
int x[MAXN];						//Store the optimal solution vector of the first ship
void dfs(int tw,int rw,int op[],int i) //Find the optimal solution of the first ship
{
	if (i>n)						//Find a leaf node
	{
		if (tw<=c1 && tw>maxw)
		{
			maxw=tw;				//Find a better solution that satisfies the conditions and save it
			for (int j=1;j<=n;j++)	//Copy optimal solution
				x[j]=op[j];
		}
	}
	else						//Not all containers have been found
	{	op[i]=1;				//Select the ith container
		if (tw+w[i]<=c1)		//Left child node pruning: load containers that meet the conditions
			dfs(tw+w[i],rw-w[i],op,i+1);
		op[i]=0;				//Do not select the ith container, backtracking
		if (tw+rw>c1)			//Right child node pruning
			dfs(tw,rw-w[i],op,i+1);
	}
}
void dispasolution(int n)		//Output a solution
{
	for (int j=1;j<=n;j++)
		if (x[j]==1)
			printf("\t Will be the first%d A container was loaded on the first ship\n",j);
		else
			printf("\t Will be the first%d A container was loaded onto the second ship\n",j);

}
bool solve()			//Solving complex loading problems
{
	int sum=0;			//Accumulated container weight remaining after the first ship is loaded
	for (int j=1;j<=n;j++)
		if (x[j]==0)
			sum+=w[j];
	if (sum<=c2)			//The second ship can be loaded
		return true;
	else				//The second ship can't be loaded up
		return false;
}

int main()
{
	int op[MAXN];				//Store temporary solution
	memset(op,0,sizeof(op));
	int rw=0;
	for (int i=1;i<=n;i++)
		rw+=w[i];
	dfs(0,rw,op,1);				//Find the optimal solution of the first ship
	printf("Solution results\n");
	if (solve())				//Output results
	{
		printf("    Optimal scheme\n");
		dispasolution(n);
	}
	else
		printf("    There is no suitable loading scheme\n");
}

2. Experiment 2 solves the m-coloring problem of graphs

[problem description] give undirected connected graphs g and m different colors. The vertices of graph G are colored with these colors, and each vertex has a color. If there is a coloring method that makes the two vertices of each edge in G have different colors, the graph is m colorable. The M coloring problem of graphs is to find out all different coloring methods for a given graph G and m colors.

[input format] the first line has three positive integers n, K and m, indicating that a given graph G has n vertices, k edges and m colors. Vertex numbers are 1, 2,..., n. In the next K rows, each row has two positive integers u and v, representing one edge (U, v) of graph G.

[output format] at the end of the program, output the calculated number of different coloring schemes. If shading is not possible, the program outputs - 1.

[input example]

5 8 4
1 2
1 3
1 4
2 3
2 4
2 5
3 4
4 5

[output example]

48

[problem solving] for figure G, the adjacency matrix A is used to store. According to the needs of solving the problem, here a is a two-dimensional array (subscript 0 is not used). When the top point i has an edge with vertex j, set ai=1, and in other cases, set ai=0.

The vertex numbers in the graph are 1 ~ n and the coloring numbers are 1 ~ M. For each vertex in graph G, the possible coloring is 1 ~ m, so the corresponding solution space is an M-ary tree, the height is n, and the level i starts from 1.

The code is as follows:

#include <stdio.h>
#include <string.h>
#define MAXN 20 				// Maximum number of vertices in a graph

int n,k,m;
int a[MAXN][MAXN];
int count=0;				//Global variable, cumulative solutions
int x[MAXN];				//Global variable, x[i] represents the shading of vertex I
bool Same(int i)			//Determine whether vertex i has the same shading as adjacent vertices
{
	for (int j=1;j<=n;j++)
		if (a[i][j]==1 && x[i]==x[j])
			return false;
	return true;
}
void dfs(int i)					//Solving the m-coloring problem of Graphs
{
	if (i>n)					//Reach leaf node
		count++;				//Increase the number of coloring schemes by 1
	else
	{
		for (int j=1;j<=m;j++)	//Explore each color
		{
			x[i]=j;
			if (Same(i))		//You can shade j to move on to the next vertex shade
				dfs(i+1);
			x[i]=0;				//to flash back
		}
	}
}
int main()
{
	memset(a,0,sizeof(a));		//a initialization
	memset(x,0,sizeof(x));		//x initialization
	int x,y;
	scanf("%d%d%d",&n,&k,&m);	//Enter n,k,m
	for (int j=1;j<=k;j++)
	{
		scanf("%d%d",&x,&y);	//Enter two vertices of an edge
		a[x][y]=1;				//Edge symmetry of undirected graphs
		a[y][x]=1;
	}
	dfs(1);						//Search from vertex 1
	if (count>0)				//Output results
		printf("%d\n",count);
	else
		printf("-1\n");
	return 0;
}

3. Experiment 3 solved the activity arrangement problem

[problem description] suppose there is a set S composed of N activities that need to use a certain resource, S={1,..., n}. This resource can only be occupied by one activity at any time. Activity i has a start time bi and an end time ei (bi < ei), and its execution time is ei bi. It is assumed that the earliest activity execution time is 0.

Once an activity starts executing, it cannot be interrupted until it is completed. If activity i and activity j have bi ≥ ej or bj ≥ ei, the two activities are said to be compatible.

Design an algorithm to find an optimal activity arrangement scheme, so that the number of activities arranged is the largest.

[problem solving]

Activity number i1234
Start time bi1246
End time ei35810

Scheduling scheme (an arrangement): x**[1], x[2],..., x[n]**

Step 1 select activity x[1]

​ ...

Step I select activity x[i]

​ ...

Step N select activity x[n]

  • Using the backtracking method to solve it is equivalent to finding an arrangement of S={1,..., n}, that is, the scheduling scheme, so that the number of all compatible activities is the largest. Obviously, the corresponding solution space is an arrangement tree.
  • It is directly implemented by the permutation tree recursive framework. For each scheduling scheme, the number of compatible activities is calculated, and the maximum number of activities maxsum is calculated by comparison. The corresponding scheduling scheme is the optimal scheduling scheme bestx, which is the solution of this problem.

solving process

  • Generate all permutations, and each permutation x=(x[1],x[2],..., x[n]) corresponds to a scheduling scheme
  • Calculate the number of compatible activities sum of each scheduling scheme
  • The maximum number of compatible activities maxsum and the optimal scheme bestx are compared

The code is as follows:

#include <stdio.h>
#include <string.h>
#define MAX 51

struct Action
{
	int b;					//Activity start time
	int e;					//Activity end time
};
int n=4;
Action A[]={{0,0},{1,3},{2,5},{4,8},{6,10}};	//Subscript 0 is not used

int x[MAX];					//Solution vector
int bestx[MAX];				//Optimal solution vector
int laste=0;				//The end time of the last compatible activity in a scenario
int sum=0;					//Number of all compatible activities in a scheme
int maxsum=0;				//Number of all compatible activities in the optimal scheme
void swap(int &x,int &y)	//Swap x, y
{	int tmp=x;
	x=y; y=tmp;
}
void dispasolution()					//Output a solution
{
	printf("Optimal scheduling scheme\n");
	int laste=0;
	for (int j=1;j<=n;j++)
	{
		if (A[bestx[j]].b>=laste)		//Select activity bestx[j]
		{
			printf("    Select activity%d: [%d,%d)\n",bestx[j],A[bestx[j]].b,A[bestx[j]].e);
			laste=A[bestx[j]].e;
		}
	}
	printf("  Number of scheduled activities=%d\n",maxsum);
}
void dfs(int i)							//Search for the optimal solution of activity problem
{
	if (i>n)							//Reach the leaf node and generate a scheduling scheme
    {
		if (sum>maxsum)
		{
			maxsum=sum;
			for (int k=1;k<=n;k++)
				bestx[k]=x[k];
		}
	}
	else
	{
		for(int j=i; j<=n; j++)				//The leaf node is not reached, considering the activity of i and n
		{	//Layer i node selection activity x[j]
			int sum1=sum;					//Save sum and last for backtracking
			int laste1=laste;
			if (A[x[j]].b>=laste)			//Activity x[j] compatible with previous
			{
				sum++;						//Number of compatible activities increased by 1
				laste=A[x[j]].e;			//Modify the last compatibility time of this scheme
			}
			swap(x[i],x[j]);				//Recursive framework for sorting tree problem: exchange x[i],x[j]
			dfs(i+1);						//Recursive framework for sorting tree problem: entering the next layer
			swap(x[i],x[j]);				//Recursive framework for sorting tree problem: exchange x[i],x[j]
			sum=sum1;						//to flash back
			laste=laste1;					//That is, the layer i node cancels the selection of activity x[j] so that other activities can be selected
		}
	}
}
int main()
{
	for (int i=1;i<=n;i++)
		x[i]=i;
	dfs(1);								//i search from 1
	dispasolution();					//Output results
}

Topics: C Algorithm data structure