ST表(Sparse Table)

ST表主要用来求解RMQ(Range Minimum/Maximum Query)问题。

ST表能够解决的问题是:

给定一个数组a[n],动态查询数组a[l]到a[r],即l到r范围内的最大值最小值。下面的讲解主要以最小值为主。

一、预处理

首先,我们预处理出数组stTable[i][j],stTable[i][j]代表从a[i]元素开始(包括a[i])连续的2^j个元素的最小值。

我们需要使用动态规划来求解stTable[i][j],初始化为stTable[i][0]=a[i]。

其中stTable[i][j] = stTable[i][j-1] + stTable[i+2^(j-1)][j-1]。举例如下:

stTable[i][1]=a[i]+a[i+1] = stTable[i][0]+stTable[i+1][0]

stTable[i][2]=a[i]+a[i+1] +a[i+2] +a[i+3] = stTable[i][1]+stTable[i+2][0]

…….

二、求出范围[l,r]中的最小值

由于stTable[i][j]存储的信息是包含2^j个元素的,所以我们需要进行拆分或者量化,如下图:

ST

我们把查询的区间[l,r]分别两个相交区间[l,l+2^j-1]和[r-2^j+1,r],即stTable[l][j]和stTable[r-2^j][j],这两个区间需要会相交,但是不影响我们求这个区间里面的最小值,即min(stTable[l][j],stTable[r-2^j][j])。

在给出区间[l,r]后,我们采用移位操作求出j的值,使得l+2^j-1<=r且l+2^(j+1)-1>r。

下面是一些基本的操作:

[c language=”++”]
//stTable的准备函数
void ST_Prepare(vector<int> a, vector<vector<int>>&stTable)
{
for (int i = a.size()-1; i >=0; i–)
{
//初始化0
stTable[i][0] = a[i];
//使用动态规划计算出stTable
for (int j=1; i + (1<<j) -1 < a.size(); j++)
{
stTable[i][j] = min(stTable[i][j – 1], stTable[i + (1<<(j-1)) ][j – 1]);
}
}
}

int queryMin(int l, int r, vector<vector<int>>&stTable)
{
int len = r – l + 1;
int k = 0;
int tmpLen = len;
//求出合适的指数k
while (tmpLen != 1)
{
k++;
tmpLen = (tmpLen >> 1);
}
return min(stTable[l][k], stTable[r – (1 << k) + 1][k]);
}
[/c]

 

转:hiho一下第76周《Suzhou Adventure》题目分析

题目大意

给定一颗N个节点的树,每个节点有权值W[i],从中选择M个节点,使得这M个节点的权值之和最大。同时满足:

  • 条件一:当一个节点被选择后,它的所有祖先节点也要被选择
  • 条件二:其中有K个节点是必须要选择的

解体思路

本题所考察的算法是树形动态规划。

对于两个附加条件,我们分别进行处理:

条件一:当一个节点被选择后,它的所有祖先节点也要被选择

该条件换一个说法,可以解释为:只有当选择了一个节点后,我们才可以选择它的子节点。

我们首先建立状态f[i][k]f[i][k]表示以i节点为根的子树,在满足条件一的情况下,选择至多k的节点能够得到的最大权值。

则可以写出状态转移情况:

  • 选择i节点:f[i][k]等于w[i]加上所有子节点选择k-1个节点的最大权值
  • 不选择i节点:f[i][k] = 0

不选择i节点很好处理,那么我们如何处理选择i节点时,子节点的情况?

根据题目描述,我们知道给定的节点i可能有多个儿子节点,不妨假设其有m个儿子节点。

则可以表示为,在m个儿子上一共选择k-1个节点,使得选择的节点权值之和最大。

举个例子,比如样例:

样例的树结构

我们在计算f[1][4]时,需要考虑对于以2,3,4分别为根节点子树,一共选择3个节点,来使得权值最大。也就是说在2子树中选择x个节点,3子树中选择y个节点,4子树中选择z个节点,使得x+y+z=3

很显然,该问题同样是一个动态规划问题:

建立状态g[i][j]g[i][j]表示前i个儿子选择j个节点时最大权值,则有状态转移过程:

g[i][j] = max{g[i - 1][j - k] + f[childId][k] | k = 0 .. j}

如果你有做过hiho一下第67期,你会发现这个子问题其实是一个泛化背包问题

因此我们可以得到整个动态规划的过程:

// 初始化f数组
f[][] = -1;

dp(root, k):
    If (k == 0) Then
        Return 0;
    End If
    If (f[root][k] != -1) Then
        // 由于可能多次调用dp(root,k)
        // 所以这里采用了记忆化的思想
        Return f[root][k];
    End If
    // 不选择该节点
    f[root][k] = 0;
    // 选择该节点
    g[][] = 0;  // 初始化g数组,需要注意g为该函数的局部变量
    For i = 1 .. m  // 枚举子节点
        For j = 0 .. k - 1
            For t = 0 .. j
                If g[i][j] < dp(child[i], t) + g[i-1][j-t] Then
                    g[i][j] = dp(child[i], t) + g[i-1][j-t];
                End If
            End For
        End For
    End For
    If (f[root][k] < g[m][k-1] + w[root]) Then
        f[root][k] = g[m][k-1] + w[root];
    End If
    Return f[root][k];

接下来我们处理条件二:其中有K个节点是必须要选择的

因为选择这K个节点,一定会选择它们所有的祖先节点。所以我们不妨先将这个K个节点和其祖先节点标记出来,并统计个数。

我们可以用下面这段代码来标记:

bool must[ N ]; // must[i]表示,该节点是否必须被选择
mark(i):
    must[i] = true;
    While (i != null)
        If (father[i] is not null) Then
            must[ father[i] ] = true;
        Else
        i = father[i];
    End While

最后我们统计所有被标记的节点数量,若其总数大于 M,那么显然是无解的。因为无论怎么选择都会超过 M个节点。

若总数小于等于 M,则表示是有可行解的。那么又有个新的问题:如何保证我们dp的过程中,一定能够把这些标记的点都选上。

我们需要在过程中,将不合法的状态标记出来,使得最后得到解一定是合法的,也就是包含所有必须选择的节点。

比如must[root] = true,那么f[root][0]一定就是一个不合法的状态,因为至少需要k=1,才能把root节点选择上。

我们建立一个标签INVALID来表示这些不合法的状态,改进后的dp函数代码:

// 初始化f数组
f[][] = -1;

dp(root, k):
    If (k == 0) Then
        If (must[root]) Then
            return INVALID;
        End If
        Return 0;
    End If
    If (f[root][k] != -1) Then
        // 由于可能多次调用dp(root,k)
        // 所以这里采用了记忆化的思想
        Return f[root][k];
    End If

    // 不选择该节点
    If (must[ root ]) Then
        f[root][k] = INVALID;
    Else
        f[root][k] = 0;
    End If

    // 选择该节点
    g[][] = 0;  // 初始化g数组,需要注意g为该函数的局部变量
    For i = 1 .. m  // 枚举子节点
        For j = 0 .. k - 1
            g[i][j] = INVALID; // 先假设不合法
            For t = 0 .. j
                If (dp(child[i], t) != INVALID and g[i-1][j-t] != INVALID) Then
                    If (g[i][j] == INVALID or g[i][j] < dp(child[i], t) + g[i-1][j-t]) Then
                        g[i][j] = dp(child[i], t) + g[i-1][j-t];
                    End If
                End If
            End For
        End For
    End For

    If (g[m][k-1] == INVALID) Then
        f[root][k] = INVALID;
    Else
        f[root][k] = g[m][k-1] + w[root];
    End If

    Return f[root][k];

另外还有一种比较取巧的方法:

由于我们动态规划得到的结果一定是最大解,所以我们只需要让最大解一定会选择这些must[i]=true的节点即可。

所以我们不妨给must[i]=true的节点加上一个很大的权值MOD。由于本题中w[i]不超过100,节点数量也不超过100,最大可能的解也就不超过10000。因此我们我们不妨设MOD = 1000000

这样在不改变dp函数情况下得到的解f[1][M],只要进行一次模运算,即f[i][M] % MOD就是该题实际的解了。

最后我们再来总结一下本题的解题过程:

  1. K个必须经过的节点执行mark()函数;
  2. 统计被标记为true的节点数量,若大于M则返回-1
  3. 若采用增加权值的方法,则对被标记为true的节点,权值增加MOD;否则直接进行第4步;
  4. 执行dp(1,M),得到f[1][M]

#1104 : Suzhou Adventure

1.题目给出若干个村庄,和这些村庄的分数,给出需要参观的村庄的总数和一定参观的村庄,求出怎么安排参观的村庄以达到最大的分数。

2.这道题目实则是一个树形动态规划的题目,我们对题目进行转化的话,会得到:

选择一定需要参观的村庄的时候,它的父亲节点也必定选取。

我们首先建立状态f[i][k]f[i][k]表示以i节点为根的子树,选择k个村庄能够得到的最大分数值。状态转移方程如下:

  • 选择i村庄:f[i][k]等于村庄i的分数加上所有子节点选择k-1个节点的最大分数值
  • 不选择i村庄:f[i][k] = 0

3.其中村庄的分数最大为100,村庄数量最多为100,所以总分最多是10000,我们给一定需要参观的村庄加上一个比较大的分数100000,这样,在动态规划的过程中,我们就会一定选取这些分数大的村庄(不一定全选,但是一旦可以到达,则必选)。

4.由于是树形,我们需要注意在统计子节点的分数时,不能反过来涉及到父节点,所以在添加边的时候,为单向边。

5.最终判断是否能够有solution时,我们判断f[1][k]/100000是否等于推荐参观村庄的数量,如果是,则输出f[1][k],否则输出-1。

更详细的解答过程可以参考:http://maybi.cn/wp_siukwan/?p=820

时间限制:10000ms
单点时限:1000ms
内存限制:256MB

描述

Little Hi is taking an adventure in Suzhou now. There are N beautiful villages in Suzhou which are numbered from 1 to N. They connected by N-1 roads in such a way that there is excactly one way to travel from one village to another. Little Hi gives each village a score according to its attractiveness. He is visiting village 1 now and plans to visit excatly M villages (including village 1) and maxize the total score of visited villages. Further more, K villages are recommended by Little Ho. He does not want to miss these recommended villages no matter what their attractiveness scores are.

Note that Little Hi visits every village on his travel route. Passing a village without visiting it is not allowed. Please find the maximum total score Little Hi can get.

输入

The first line contains 3 integers N(1 <= N <= 100), K(1 <= K <= 5) and M(1 <= M <= N), representing the number of villages, the number of recommended villages and the number of villages Little Hi plans to visit.
The second line contains N integers, the attractiveness scores of villages. The scores are between 1 to 100.
The third line contains K integers, the list of recommended villages.
The following N-1 lines each contain two integers a and b, indicating that village a and village b are connected by a road.

输出

The maximum scores Little Hi can get. If there is no solution output -1.

样例输入
5 2 4
1 2 3 4 5
3 4
1 2
1 3
1 4
2 5
样例输出
10

AC代码:
[c language=”++”]
//#include<string>
//#include <iomanip>
#include<fstream>
//#include<set>
//#include<queue>
#include<map>
//#include<unordered_set>
//#include<unordered_map>
//#include <sstream>
//#include "func.h"
//#include <list>
#include<stdio.h>
#include<iostream>
#include<string>
#include<memory.h>
#include<limits.h>
//#include<stack>
#include<vector>
#include <algorithm>
using namespace std;

/*
5 2 2
1 2 3 4 5
3 5
1 2
1 3
1 4
2 5

*/

#define MOD 100000
struct VillageNode{
int score;
vector<int> nb;
VillageNode() :score(0), nb(0){};
};
int dfs(vector<VillageNode>&village,vector<vector<int>>&f, int root, int k)
{
if (k == 0)
return 0;
else if (f[root][k] != -1)
return f[root][k];//如果之前已经计算过f[root][k],则直接返回,不用遍历

//不选择这个节点的时候
f[root][k] = 0;

//选择这个节点的时候

int m = village[root].nb.size();

//建立状态g[i][j],g[i][j]表示前i个儿子选择j个节点时最大权值
//g[i][j] = max{g[i – 1][j – t] + f[childId][t] | t = 0 .. j}
vector<vector<int>> g(m + 1, vector<int>(k, 0));

//枚举root的孩子节点,需要注意的是,枚举的是儿子节点,不能使用双向边
for (int i = 1; i <= m; i++)
{
//因为选择当前节点,所以最多是求k-1个儿子的分值总和
for (int j = 0; j < k; j++)
{
for (int t = 0; t <= j; t++)
{
int child = village[root].nb[i-1];
if (g[i][j] < dfs(village, f, child, t) + g[i – 1][j – t])
{
g[i][j] = dfs(village, f, child, t) + g[i – 1][j – t];
}
}
}
}

if (f[root][k] < g[m][k – 1] + village[root].score)
f[root][k] = g[m][k – 1] + village[root].score;
return f[root][k];

}
/*
函数名 :main
函数功能:主函数
*/
int main(void)
{
int villageCount;
int recommendedVillageCount;
int planCount;

scanf("%d %d %d", &villageCount, &recommendedVillageCount, &planCount);

vector<VillageNode> village(villageCount+1);
//输入各个村庄的分数
for (int i = 1; i <=villageCount; i++)
{
scanf("%d", &village[i].score);
}

//输入推荐的村庄
for (int i = 0; i < recommendedVillageCount; i++)
{
int tmp;
scanf("%d", &tmp);
village[tmp].score += MOD;
}

//输入邻居关系
for (int i = 0; i < villageCount – 1; i++)
{
int a, b;
scanf("%d %d", &a, &b);
village[a].nb.push_back(b);
//需要注意的是,浏览的是儿子节点,不能使用双向边
//village[b].nb.push_back(a);
}

vector<vector<int>>f(villageCount + 1, vector<int>(planCount + 1, -1));

dfs(village, f, 1, planCount);
if (f[1][planCount] / MOD == recommendedVillageCount)
printf("%d\n", f[1][planCount] % MOD);
else
printf("-1\n");

return 0;
}

[/c]

牛客网的三道简单练习

[编程题] 斐波那契数列
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。
[c language=”++”]
class Solution {
public:
int Fibonacci(int n) {
if(n==0) return 0;
else if(n==1) return 1;
else
{
int dp1=0;
int dp2=1;
for(int i=2;i<=n;i++)
{
int tmp=dp2;
dp2=dp2+dp1;
dp1=tmp;
}
return dp2;
}
}
};
[/c]

[编程题] 二进制中1的个数
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
[c language=”++”]
class Solution {
public:
int NumberOf1(int n) {
int result=0;
int check=1;
for(int i=0;i<sizeof(int)*8;i++)
{
if((check&n)!=0)
result++;
check=(check<<1);
}
return result;
}
};
[/c]

[编程题] 反转链表
输入一个链表,反转链表后,输出链表的所有元素。
[c language=”++”]
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
ListNode* now=pHead;
ListNode* preHead=new ListNode(0);
preHead->next=now;

if(now==NULL) return now;
while(now->next)
{
ListNode* tmp=now->next;
now->next=tmp->next;
tmp->next=preHead->next;
preHead->next=tmp;
}
return preHead->next;

}
};
[/c]

#1106 : Koch Snowflake

1.题目主要是给出了科赫雪花分形,给出变形的代数n和边的编号i,求出这条边属于第几代。

2.首先我们来看看从第n-1代到第n代的变化,设第n-1代中的边i,如下图:

QQ截图20151223091034

我们看到,在第n-1代中的边i,变形到第n代时,会演化成为4(i-1)+1、4(i-1)+2、4(i-1)+3、4(i-1)+4.

其中4(i-1)+2和4(i-1)+3是第n代的边,而4(i-1)+1、4(i-1)+4是第n-1代的边。

那么现在我们明确了,如果某边的编号i%4==2或==3,那么这条边就是第n代的,否则,返回到上一代中继续求。返回到上一代时,4(i-1)+1、4(i-1)+4对应的是i,那么假设边k需要返回到上一代,那么应该k/4向上取整(例如1/4=1,4/4=1),然后继续迭代。

需要使用递归求解。

时间限制:10000ms
单点时限:1000ms
内存限制:256MB

描述

14214811509136.png

Koch Snowflake is one of the most famous factal. It is built by starting with an equilateral triangle, removing the inner third of each side, building another equilateral triangle at the location where the side was removed, and then repeating the process indefinitely.

Let Kn be the Koch Snowflake after n-th iteration. It is obvious that the number of sides of Kn, Nn, is 3*4n. Let’s number the sides clockwisely from the top of Koch Snowflake.

Let si,n be the i-th side of Kn. The generation of si,n is defined as the smallest m satifying si,n is a part of the sides of Km. For example, in the above picture, the yellow sides are of generation 0; the blue sides are of generation 1; the red sides are of generation 2.

Given a side si,n, your task is to calculate its generation.

输入

The input contains several test cases.

The first line contains T(T <= 1000), the number of the test cases.

The following T lines each contain two numbers, i(1 <= i <= 10^9) and n(0 <= n <= 1000). Your task is to calculate the generation of side si,n.

输出

For each test case output the generation of the side.

样例输入
5
1 0
1 1
2 1
10 2
16 3
样例输出
0
0
1
2
0

 

下面是AC的源代码:

[c language=”++”]
//#include<string>
//#include <iomanip>
#include<fstream>
//#include<set>
//#include<queue>
#include<map>
//#include<unordered_set>
//#include<unordered_map>
//#include <sstream>
//#include "func.h"
//#include <list>
#include<stdio.h>
#include<iostream>
#include<string>
#include<memory.h>
#include<limits.h>
//#include<stack>
#include<vector>
#include <algorithm>
using namespace std;
/*
1.题目主要是给出了科赫雪花分形,给出变形的代数n和边的编号i,求出这条边属于第几代。

2.首先我们来看看从第n-1代到第n代的变化,设第n-1代中的边i,如下图:

4(i-1)+2 4(i-1)+3
/\
i 4(i-1)+1 / \ 4(i-1)+4
———- —– ——
第n-1代 第n代

我们看到,在第n-1代中的边i,变形到第n代时,会演化成为4(i-1)+1、4(i-1)+2、4(i-1)+3、4(i-1)+4.

其中4(i-1)+2和4(i-1)+3是第n代的边,而4(i-1)+1、4(i-1)+4是第n-1代的边。

那么现在我们明确了,如果某边的编号i%4==2或==3,那么这条边就是第n代的,否则,返回到上一代中继续求。
返回到上一代时,4(i-1)+1、4(i-1)+4对应的是i,那么假设边k需要返回到上一代,那么应该k/4向上取整(例如1/4=1,4/4=1),然后继续迭代。

*/
int dfs(int side, int generation)
{
if (generation == 0)
return 0;
else
{
if (side % 4 == 2 || side % 4 == 3)
return generation;
else
{
int i = side / 4;
if (side % 4) i++;
return dfs(i, generation – 1);
}
}
}
/*
函数名 :main
函数功能:主函数
*/
int main(void)
{
int n;
cin >> n;
while (n–)
{
int side, generation;
cin >> side >> generation;
cout << dfs(side, generation) << endl;
}
return 0;
}
[/c]

 

319. Bulb Switcher(Medium)

1.该问题是算法设计问题中的一个经典问题:开关翻转问题。

2.根据题意,我们可以把题目转化为,被翻转偶数次的灯是灭的,被翻转奇数次的灯是亮的。

3.那么问题可以继续转化为:什么编号的灯被翻转奇数次?

4.进一步转化:编号为n的灯,n有若干个因数(注意不是质因数),那么它会被它的因数进行翻转操作,也就是说,如果有奇数个因数,那么编号为n的灯就会被翻转奇数次,即最终是亮的。如数字4,因数为1、2、4;数字9号,因数为1、3、9;

5.问题可以继续转化:如果判断数字n的因数有奇数个?

每个数都可以化简成质因数的乘式n=(a[i]^p[i])*(a[i+1]^p[i+1])…….其中a[i]为质数,p[i]为数字n包含质数a[i]的多少次方。

那么n的因数总数为:(p[i]+1)*(p[i+1]+1)…….

使得因数的总数为奇数,那么每个项都应该是奇数,即p[i]+1为奇数,那么p[i]为偶数。

如果p[i]为偶数,那么a[i]^p[i]为a[i]^2、a[i]^4、a[i]^6等等,即这些数是为完全平方数,例如4,9,16,25等等。

6.所以,题目要求的是,n以内的完全平方数的个数。

AC代码如下:
[c language=”++”]
class Solution {
public:
int bulbSwitch(int n) {
int result = 0;
for (int i = 1; i*i <= n; i++)
result++;
return result;
}
};
[/c]
 

arp包导致的网络拥挤

1.现象

前些天实验室的网络开始变得很慢,一开始我们以为是断网,后来我们发现,其实是能够上网的,只是网速非常慢。而且隔壁两个实验室却没有这样的情况。

2.查找

听宁哥说他同学用wireshark抓包,发现有一台惠普的机器在疯狂的发送arp包,我们怀疑是不是实验室的惠普打印机出问题了。于是下载了wireshark来进行抓包。

IMG_1103

我们会发现,有两台机器在疯狂地发送arp包,其中有Microsof_02和Elitegro_e3,他们都在广播arp包,似乎在解析192.168.1.100地址的时候出现了问题。我们可以初步地定位网络拥挤的原因,就是因为这两台机器在不停地广播arp包。

我们来看看arp包的内容:

IMG_1098

 

似乎除了机器的MAC地址外,查不到有效的信息。

IMG_1105

 

我们看到黄色字,提示我们这个192.168.1.100的ip被别人使用了。那么问题就变成了,现在有两台不同mac地址的机器在占用同一个ip。

其中数据包中也没看到什么有用的信息了。

于是我使用了arp -a的命令,来查看,这些MAC地址对应的机器IP是多少:IMG_1104

 

结果发现,我们并没有找到这些MAC地址,所以我还是没办法定位这些机器。

直到,第二天早上,实验室的同学找到了其中一个mac地址对应的ip是172.18.219.240。而更神奇的是,宁哥刚还有另外实验室的同学电脑ip恰好就是240。172的ip是整个学院楼的ip段,我们当初也没有想到是其他实验室的机器导致我们网络拥挤。

后来,宁哥告诉我们,他的同学新买了一个路由器,然后把原来电脑的mac地址复制到路由器上面了,导致arp的时候发现有两台设备有回应,进一步导致了上述的arp广播风暴。后来把路由器拔掉了就没事了,拯救了整个实验室!

对于上面的过程,其实我还是有比较多的疑问:

1. 192网段的ARP包为什么会广播到172的网段?

2.为什么那两台机器会狂发arp包?而且从包中可以看出他们的mac地址明显不一样?

这些问题需要待后续深入研究和了解才能进行补充了~

 

 

 

37. Sudoku Solver(Hard)

1.这道题目主要是使用回溯法进行遍历,其中需要注意的是进行剪枝。剪枝情况如下:

首先是遍历可以填充的数字,如果遍历完后都没有可以填充,那么就直接回溯,不用继续遍历接下来的空格。

否则会出现这种情况,前面还没填充就去填充后面的:

QQ截图20151223091034

另外最基本的就是用hash检测行列和方块里面的数字是否已经被使用,其中方块数字的index应该为[i/3*3+j/3]。

AC代码如下:

[c language=”++”]
class Solution {
public:
vector<vector<bool>> HashRow;
vector<vector<bool>> HashRec;
vector<vector<bool>> HashCol;
void dfs(vector<vector<char>>& board,int&leftNumber,bool&isFilled)
{
//printfVectorInt2(board);
//printf("\n");
if (leftNumber == 0)
isFilled = true;
else
{
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
if (board[i][j] == ‘.’)
{
bool noNum = true;
for (int num = 1; num <= 9; num++)
{
if (!HashRow[i][num] && !HashCol[j][num] && !HashRec[i / 3 * 3 + j / 3][num])
{
noNum = false;
HashRow[i][num] = true;
HashCol[j][num] = true;
HashRec[i / 3 * 3 + j / 3][num] = true;

leftNumber–;
board[i][j] = num+’0′;
dfs(board, leftNumber, isFilled);
if (isFilled)
return;//已经填充成功

//填充失败,还原情况
board[i][j] = ‘.’;
leftNumber++;
HashRow[i][num] = false;
HashCol[j][num] = false;
HashRec[i / 3 * 3 + j / 3][num] = false;
noNum = true;
}
}
if (noNum) return;
}
}
}
}
}

void solveSudoku(vector<vector<char>>& board) {
HashRow = vector<vector<bool>>(10, vector<bool>(10, false));
HashRec = vector<vector<bool>>(10, vector<bool>(10, false));
HashCol = vector<vector<bool>>(10, vector<bool>(10, false));
vector<int> CountRow(9, 0);
vector<int> CountRec(9, 0);
vector<int> CountCol(9, 0);
int leftNumber = 0;//仍需要填充的数字个数
for (int i = 0; i < board.size(); i++)
{
for (int j = 0; j < board[i].size(); j++)
{
if (board[i][j] != ‘.’)
{//记录已经出现过的数字
CountRow[i]++;
CountCol[j]++;
CountRec[i / 3 * 3 + j / 3]++;
HashRow[i][board[i][j]-‘0’] = true;
HashCol[j][board[i][j]-‘0’] = true;
HashRec[i / 3 * 3 + j / 3][board[i][j] – ‘0’] = true;
}
else
leftNumber++;
}
}
bool isFilled = false;
dfs(board, leftNumber, isFilled);
}
};
[/c]

GPU编程–CUDA环境配置与简单例程

相关文章:
智力拼图问题–关于回溯和并行:单线程到多线程再到GPU编程的进阶(一)
智力拼图问题–关于回溯和并行:单线程到多线程再到GPU编程的进阶(二)
智力拼图问题–关于回溯和并行:单线程到多线程再到GPU编程的进阶(三)

最近需要使用回溯法来完成算法设计的作业,在经历了单线程编程和多线程编程后,发觉效果并不是很理想,于是考虑采用GPU编程,计划通过GPU写一个简单的DFS算法。

本文的目的是快速上手使用GPU和进行简单的编程。首先推荐一本书,在这本书的帮助,我迅速学会了基本的GPU编程。

《CUDA并行程序设计:GPU编程指南》

推荐

一、环境配置

1.由于达到快速开发的目的,我直接采用windows平台进行开发,IDE采用的是VS2013.

2.在英伟达的官网上,有CUDA toolkit的下载地址:https://developer.nvidia.com/cuda-downloads

3.CUDA,Compute Unified Device Architecture,是一个运算平台。我们借助这个运算平台去使用GPU进行数据处理。

4.windows版本的安装比较简单,直接在官网下载:

CUDA

5.接着按照提示安装即可,前提是安装了VS2013或者VS2010\2012。

二、运行例程

安装了一个toolkit后,比较麻烦的事情是配置环境路径,配置项目属性,幸好CUDA提供了一系列的例程,让我们免去了配置的麻烦。CUDA开发者网址:http://docs.nvidia.com/cuda/cuda-samples/index.html

安装完毕后,我们找到CUDA的例程,我的CUDA是安装在C盘:C:\ProgramData\NVIDIA Corporation\,CUDA的例程在C:\ProgramData\NVIDIA Corporation\CUDA Samples\v7.5中,我们可以把例程复制出来,然后打开例程

打开后,我们重点关注cppintegration,因为这个例程的划分比较合理,分为main.cpp和cppintegration.cu文件,main.cpp主要是电脑端的代码,cppintegration.cu则主要是设备端的代码。

cppIntegration

 

在写代码的时候需要注意,如果我们修改了cppintegration.cu文件里面的代码,那么IDE就会调用CUDA的编译器来进行编译和链接,这个编译链接过程是比较慢的,而只修改main.cpp文件的话,IDE只会重新编译main.cpp,然后重新把.cu生成的文件连接到主程序上,速度会比较快。

[c language=”++”]

// run the device part of the program
bTestResult = runTest(argc, (const char **)argv, str, i2, len);

[/c]

main.cpp代码中,这个代码是调用设备端(即显卡)的函数,我们可以在cppintegration.cu中看到这段代码的定义。

主要的流程如下:

申请显存–>把数据从内存复制到显存–>在显存中进行数据处理–>把数据从显存中复制到内存–>结束

其中,在复制内存的时候也要特别注意,一定需要连续的内存空间!相关文章:

CUDA的内存复制:关于GPU的数据复制问题

CUDA提供了非常丰富的例程中,如有时间,可以选择适合自己的进行研究。而接下来的程序修改,我们都将基于cppintegration项目中的环境。

三、GPU编程特点

(1)基本语法

在GPU编程里面,我们会把函数分为三个类型:__device__、__host__和__global__,对于这三个类型标识符,在关于CUDA里面的global、host、device文章里已经作了解释。

假如我们编写了GPU函数(也称之为内核):

__global__ void runTest() {int a,int b}

那么,如何让GPU来跑这个函数呢?

在调用GPU函数(内核)时,我们需要按照下面的格式:

runTest<<<blocks,threads>>>(a,b)

其中blocks表示需要开启的线程块数量,threads表示每个线程块上开启的线程数。例如:

runTest<<<2,128>>>(a,b)

我们相当于开启了2*128条线程,即并行执行runTest函数256次。

(2)如何标识每一条线程(每一个函数)?

同时开启了256次runTest后,我们如何区分不同线程的runTest?这个时候我们需要使用到blockIdx.x,blockDim.x和threadIdx.x,假设我们运行runTest<<<2,128>>>(a,b)

blockIdx.x表示我们开启的线程块ID,对应上面的程序,blockIdx.x的范围为0到1;

blockDim.x表示每个线程块开启的线程数量,对应上面的程序,blockDim.x为128;

threadIdx.x则表示当前线程在其所在的线程块中的编号。

我们通过const unsigned int tid = (blockIdx.x*blockDim.x) + threadIdx.x;就可以得到线程的id号,而接下来的编程,我们会把线程的id作为数组的下标,从而实现并行计算。

四、编程实践

有了上述的初步理解后,我们就可以进行GPU编程,我们来做一个简单的实现,计算数组a和数组b下标相同的对应元素的积,并存放到数组c中,即c[i]=a[i]*b[i],然后再使用for循环使c[i]++ 10240次

可能你们会想到,这使用for循环不是很容易解决吗?好的,为了凸显显卡强大的并行计算能力,我们把数组a和数组b的长度定义为1024*1024(略超过100万)。下面我们进行编程实现:

简单的for循环实现:

[c language=”++”]
void multipleCPU(int *const a_gpu, int *const b_gpu, int *const c_gpu)
{
for (int i = 0; i <1024*1024; i++)
{
c_gpu[i] = a_gpu[i] * b_gpu[i];
for (int j = 0; j <10240; j++)
c_gpu[i]++;
}
}
[/c]

而GPU编程,我们把计算放在内核中,开启1024个线程块,每个线程块开启1024条线程,即总共开启1024*1024(100万)条线程进行数据处理,显卡处理100万条线程还是十分轻松的。

需要注意的是,目前大部分英伟达显卡,单个线程块支持的最大线程数是1024条,我们可以通过打印显卡的信息获得相关信息:

最大线程数

 

下面是GPU的内核实现和接口调用代码:

 

[c language=”++”]
/*
函数名 :gpuTestSiukwan
函数功能:计算a_gpu[i] * b_gpu[i],并把结果存放到c_gpu[i]中,
输入参数:
a_gpu :被填充的矩阵
b_gpu :用来填充的图形
c_gpu :被填充矩阵的第一个空格位置x
*/
__global__ void gpuTestSiukwan(int *const a_gpu, int *const b_gpu, int *const c_gpu)
{
// write data to global memory
const unsigned int tid = (blockIdx.x*blockDim.x) + threadIdx.x;
c_gpu[tid] = a_gpu[tid] * b_gpu[tid];
for (int i = 0; i <10240; i++)
c_gpu[tid]++;
}

//接口函数
extern "C" void runCUDA_AddOne(int blocks, int threads, int *const a_gpu, int *const b_gpu, int *const c_gpu)
{
gpuTestSiukwanAddOne<<<blocks, threads >>>(a_gpu, b_gpu, c_gpu);
}
[/c]

当然,还有基本的数据转移操作 ,从内存复制到显存,再从显存复制到内存:

[c language=”++”]
//定义gpu上的变量
int *a_gpu, *b_gpu, *c_gpu;

//在gpu上分配内存
cudaMalloc((void **)&amp;amp;a_gpu, sizeof(int) * 1024);
cudaMalloc((void **)&amp;amp;b_gpu, sizeof(int) * 1024);
cudaMalloc((void **)&amp;amp;c_gpu, sizeof(int) * 1024);

//把数据从内存复制到显存
cudaMemcpy(a_gpu, a_cpu, sizeof(int) * 1024, cudaMemcpyHostToDevice);
cudaMemcpy(b_gpu, b_cpu, sizeof(int) * 1024, cudaMemcpyHostToDevice);

//////////////////////////////////////////////////////
//CUDA运算
//////////////////////////////////////////////////////
runCUDA(blocks, threads,a_gpu, b_gpu, c_gpu);

//把在gpu中处理完毕的数据从显存复制到内存
cudaMemcpy(c_cpu, c_gpu, sizeof(int) * 1024, cudaMemcpyDeviceToHost);

//释放相关变量的显存
cudaFree(a_gpu);
cudaFree(b_gpu);
cudaFree(c_gpu);
[/c]

 

详细的代码,已经放到github上:https://github.com/siukwan/algorithm

大家可以上去下载下来,运行一下。

实验数据如下,可以看到单纯使用GPU运算,耗时45秒,而使用GPU运算,基本在1秒内:

实验数据

当然,GPU的每个线程并不能进行十分复杂的操作,例如我进行多次递归操作后,GPU驱动会崩溃,所以在设计算法的时候要注意这一点:

崩溃

五、GPU编程进阶

当初使用学习GPU编程,是为了完成算法设计的作业,相关文章如下:

相关文章:
智力拼图问题–关于回溯和并行:单线到多线程再到GPU编程的进阶(三)

这道题目还是十分有意思的,而实验结果也是十分惊人的,下面为在对这道题目进行实验室得出的部分实验数据对比举例:

gpu实验结果

gpu实验结果2

目前英伟达在官网上面提供了很多工具包,也就是说不需要亲自写这么底层的代码,对指针进行操作,对数据进行从内存到显存的复制和从显存到内存的复制。CUDA在深度学习、图像处理、大数据分析等等方面都有极为重要的应用,我们应该充分发挥英伟达显卡的性能。有兴趣的童鞋可以浏览CUDA的网址进一步了解:https://developer.nvidia.com/cuda-zone

cuda平台

智力拼图问题–关于回溯和并行:单线程到多线程再到GPU编程的进阶(三)

相关文章:
智力拼图问题–关于回溯和并行:单线程到多线程再到GPU编程的进阶(一)
智力拼图问题–关于回溯和并行:单线程到多线程再到GPU编程的进阶(二)
智力拼图问题–关于回溯和并行:单线程到多线程再到GPU编程的进阶(三)
GPU编程–CUDA环境配置与简单例程

这一次,我们终于涉及到GPU了!通过GPU编程去求解智力拼图问题!

目前我在自身的windows8.1笔记本上面采用GPU编程,使用了英伟达CUDA7.5的toolkit,IDE采用的是VS2013.按照和示范教程可以参考:

 GPU编程–CUDA环境配置与简单例程

正式转入正文!

一、多线程的模型

通过上一篇文章,我们知道,多线程编程,就是把一开始的12个图形,分别使用一条线程去遍历,相当于本来的状态空间树是12层的,现在减少为11层。

而GPU编程中,涉及到上万条线程,都是很常见的,那我们应该如何把状态空间树划分为成千上万条线程呢?下面给出一个解决方案:

在多线程编程中,我们划分为12条线程,如下图:

多线程

那做一个简单的转换,我们多深入数层,岂不是就能够划分更多的线程?如下图:

深入到第二层:

第二层

深入到第三层:

第三层

存在的问题:到了这里,你可能意识到,存在一些情况,在第二层就已经不合适了,即没有必要进入第三层。这是这个方法的一个缺陷,但是基于GPU强大的并行计算能力,我们先忽略这个问题,讨论每个线程的工作任务。

二、进行实验

我们假设深入到第三层,开启11880条线程,这对GPU来说确实是很轻松的事情。那么我们接下来讨论每条线程该做些什么样的工作。深入到第三层,树的深度是12,意味着我们还需要在每条线程里面进行深度搜索,最大的递归深度为12-3=9层。我们每条线程需要DFS9层。

好的,我们来实际操作。但是实际上,你们会发现,GPU能够进行大量的并行计算,是以牺牲计算性能来实现的,也就是说,GPU的每条线程,其运算能力是很弱的,分配到每条线程的内存空间也是十分有限的。我们在实验过程中,递归到第三层,就会出现显卡驱动崩溃的情况!如下图:

崩溃

 

好吧,你们可能觉得我的显卡不够好,我的显卡型号是GeForce 840M,确实不是一个好显卡,那么我们来找一个好一点的显卡进行实验–GTX 960M!实验结果如下图:

高级崩溃

很遗憾,依然崩溃了。我们充分体会到每个GPU线程计算能力的脆弱性。

三、再对多线程模型进行探讨

通过上面的实验室我们知道GPU线程无法递归那么深的深度(超过2深度就崩溃),无论你开启了一条GPU线程还是1万条GPU线程,只要递归深度超过了2,就崩溃了。

那既然GPU运算能力有限,我们能不能改变我们的多线程计算模型(并行计算模型)?经过两天的实验和探索,我们确实是找到了!

对于上面的遍历,我们采用的是DFS(深度优先搜索),深度优先搜索通过递归不断遍历,必然会导致栈的开销较大。那么我们是否可以采用BFS(广度优先搜索)?采用广度优先搜索,我们就可以避免了调用函数而导致开启的栈的开销

还记得在第二点中提到的存在问题不?在每一层,都会有一些废弃的节点,假如我们按照bfs进行搜索,相当于对树进行层序遍历,这样就可以避免了废气的节点还会深入到下一层的情况!

既然有一定的合理性,那我们来重新编写程序来解决智力拼图!

四、使用BFS的并行计算模型

我们可以简单的理解为树的层序遍历。我们使用gpu对每一层的节点分配一个线程,而gpu运算只计算它下一层的可行解。

当该层的线程全部执行完毕,我们就有了下一层的可行解,也就是下一层的节点,这时,我们再次使用gpu对下一层的节点进行分配线程和计算。如此类推,理论上,我们使用调用gpu12次,那么就可以把这个12层的树给遍历完。

而且这种算法,在每一层遍历的时候,就已经去掉了一些不可能的情况,使得下一层的节点大大减少!如下图:
11111

222222334

按照这个思想,我们编写了程序,代码在github上面:https://github.com/siukwan/algorithm

BFS流程图

 

各层次线程开启数量:

线程数量

我们可以看到红框框住的几个数据,发现其需要处理的情况数量超过了200万,而实际测试知道我的显卡GT 840M最多只能开200万条线程,所以在这里我才用了循环拆分,为了减轻负担,我并没有同时开启200万条线程,设置了最多开启100万条线程,所以超过100万种情况的,我拆分为多次处理,每次最多处理100万种情况。

五、实验结果

使用了GPU后,其强大的线程并行能力着实让我们很吃惊,同时开启100万条线程毫无压力(1024个blocks,每个blocks开启1024条线程,共1024*1024)

gpu实验结果

gpu实验结果2