[OpenMP] 并行计算入门
OpenMP并行计算入门
个人理解
OpenMP是一种通过共享内存并行系统的多处理器程序设计的编译处理方案,通过预编译指令告诉编译器哪些代码块需要被并行化,通过拷贝代码块实现并行程序。对于循环的并行化我的理解大概是这样的:
- 首先,将循环分成线程数个分组,每个分组执行若干个指令,一个分组代表一个线程
- 其中有一个为主线程,其他的均不是主线程,每个分组分别执行自己组内的代码
- 当所有组别的代码执行完毕之后,在最后会和,通过主线程将结果带回
- 关闭其他所有线程(只留下主线程)
我觉得openmp编程中最需要注意的就是哪些代码块可以被并行化,如果不能,怎么修改能让他可以被并行化,以及考虑好数据依赖、循环依赖、数据竞争等问题,保证运算结果的准确性。
配置
我使用的是ubuntu18.04,配置起来比较简单,一般需要安装gcc的。
sudo apt-get install gcc
在编译之前我们需要加上-fopenmp,然后运行就可以执行我们带openmp的程序了。
gcc -fopenmp filename.cpp -o filename
然后我们运行就可以了。
指令学习
parallel
#pragma omp parallel
parallel的作用是使得其作用的代码块被并行执行n次,n默认为cpu核心数目,也可以通过set方法预先设置parallel的线程数目。
omp_set_num_threads(8);
int num = 0;
#pragma omp parallel private(num)
{
int num = omp_get_thread_num();
printf("thread num:%d\n",num);
fflush(stdout);
}
/*
我解决了输出问题。。
thread num:0
thread num:4
thread num:3
thread num:2
thread num:1
thread num:5
thread num:6
thread num:7
*/
for
#pragma omp parallel for[clause[claue..]]
#pragma omp parallel for
for(int i=0;i<10;i++){
//cout<<omp_get_thread_num()<<endl;
printf("thread num is:%d\n",omp_get_thread_num());
fflush(stdout);
}
/*
输出结果:
thread num is:0
thread num is:0
thread num is:0
thread num is:1
thread num is:2
thread num is:2
thread num is:1
thread num is:1
thread num is:3
thread num is:3
*/
for循环使用parallel for指令很简单,只是要考虑到并行时候的循环依赖问题,下面会讨论到。
注意
从这里开始我都会用printf进行输出,因为我试了试cout输出,发现输出老是有问题,就是会输出空白行,我认为是在并行过程中cout<<value和<<endl被当成了两个代码代码块,当有的线程输出了换行符之后,有的线程刚好也输出换行符,就会导致我们看到很多换行符。所以解决这个问题最好的办法是用printf输出,就不会出现那样的问题了。
sections
sections与for不同的是将代码块分成不同的功能模块,而for做的是将代码块分成不同的数据模块,其特点是“功能并行”,for的并行特点是“数据并行”。例如,通过sections将代码块分成不同的section,每个section可以是功能不同的函数,这就是与for最大的不同。
#pragma omp parallel sections
{
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
}
/*输出结果
thread num:1
thread num:1
thread num:0
thread num:3
thread num:1
thread num:2
*/
哪个section执行哪个指令完全取决于实现;如果section个数多于线程数,每个section都将会被执行,但是如果section数少于线程数,有些线程就不会执行任何操作。如果omp后面不加parallel,默认section是按照串行的顺序执行的,只有加了parallel,才会按照并行的方式执行section。
single
single指令效果是让一条代码块的语句只由一个线程来处理。
void func(){
printf("hello");
//cout<<"hello"<<endl;
}
#pragma omp single
{
func();
}
/*
输出结果(只由一个线程执行一次):
hello
*/
master
master指令表示只能由主线程来执行,其他线程不能执行该代码块的指令。
#pragma omp parallel
{
#pragma omp master
{
for(int i=0;i<10;i++){
cout<<"thread num is:"<<omp_get_thread_num()<<",i:"<<i<<endl;
}
}
}
/*
可以看出来,只有master线程执行了我们的for循环,输出有序。
输出结果:
thread num is:0,i:0
thread num is:0,i:1
thread num is:0,i:2
thread num is:0,i:3
thread num is:0,i:4
thread num is:0,i:5
thread num is:0,i:6
thread num is:0,i:7
thread num is:0,i:8
thread num is:0,i:9
*/
critical
critical指令表明该指令包裹的代码块只能由一个线程来执行,不能被多个线程同时执行。注意,如果多个线程试图执行同一个critical代码块的时候,其他线程会被堵塞,也就是排队等着,直到上一线程完成了代码块的操作,下一个线程才能继续执行这一代码块。
与single不同的是,single只能执行一次,并且是单线程执行,执行完了就不会再执行了,而critical可以多次执行。
int sum = 0;
#pragma omp parallel for shared(sum)
for(int i=0;i<10;i++){
#pragma omp critical
{
printf("thread:%d,sum:%d\n",omp_get_thread_num(),sum);
sum++;
}
}
cout<<sum<<endl;
/*输出结果
thread:0,sum:0
thread:0,sum:1
thread:0,sum:2
thread:2,sum:3
thread:2,sum:4
thread:1,sum:5
thread:1,sum:6
thread:1,sum:7
thread:3,sum:8
thread:3,sum:9
10
*/
从句
可以通过参数private将变量变成每个线程私有的变量,这样,其他线程对该变量的修改不会影响其他线程的变量。这里空说可能不理解,但是可以通过下面的例子来理解。先来看看什么是循环依赖。
依赖(循环依赖)
#include <iostream>
#include <omp.h>
using namespace std;
int main(){
int fib[1000];
fib[0] = 1;
fib[1] = 1;
#pragma omp parallel for
for(int i=2;i<20;i++){
fib[i] = fib[i-2] +fib[i-1];
}
for(int i=0;i<20;i++){
cout<<i<<":"<<fib[i]<<endl;
}
return 0;
}
/*
输出结果:
0:1
1:1
2:2
3:3
4:5
5:8
6:13
7:6
8:6
9:12
10:18
11:30
12:0
13:0
14:0
15:0
16:0
17:0
18:0
19:0
*/
可以看到,后面很多数据的结果都是0,还有很多算的是错的(7之后的数据),为什么呢?因为这里有循环依赖。循环依赖指的是不同线程之间有变量之间的依赖,这个例子中,由于线程之间有变量之间的依赖,所以必须要前面的线程算完了,后面的线程再在前面线程算的结果的基础上来算才能算出正确的结果,有些时候呢,我们可以通过更改逻辑来避免循环依赖,从而使循环在并行下也可以得到计算,并且不会算错。给个例子(这个例子是网上找的):
double factor = 1;
double sum = 0.0;
int n = 1000;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < n; i++)
{
sum += factor / (2 * i + 1);
factor = -factor;
}
double pi_approx = 4.0*sum;
cout<<pi_approx<<endl;
//上面的例子会输出错误的值,这是因为不同线程之间有循环依赖
//消除循环依赖后
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < n; i++)
{
if(i%2){
factor = 1;
}else{
factor = -1;
}
sum += factor / (2 * i + 1);
}
double pi_approx = 4.0*sum;
cout<<pi_approx<<endl;
/*这下消除了循环依赖,但结果其实依然不正确,
这是因为不同的线程之间的factor其实还是shared的,
这样一个线程给shared赋值的时候可能会影响到其他线程对
sum求和时读取factor变量的错误,因此我们只需要再改一步,
让factor变为private即可。
*/
#pragma omp parallel for reduction(+:sum) private(factor)
schedule
schedule是对循环并行控制中的任务调度的指令,例如,我们在写parallel for的时候,我们开n个线程去并行执行一段for循环代码,但是我们怎么知道哪个线程执行哪次迭代的代码呢?我们能不能控制呢?schedule就是做这个工作的。parallel for下我们默认是通过static进行调度的,每个线程分配chunk_size个迭代任务,而这个chunk_size是通过迭代数除以线程数算出来的(最后一个线程可能分配的少一些)。
type
- static:静态分配任务,指的是分配的过程跟运行的时候任务分配无关
- dynamic:动态调度,不指定size,则按照谁先执行完任务下一次迭代就分配给谁,无法提前预知哪个线程执行哪次迭代。而指定size之后,表示每次执行完任务之后分配给一个线程的迭代任务数量。
- guided:
- runtime:根据环境变量确定是上面调度策略中的哪种。
#pragma omp parallel schedule(type,size)
//这里type可以选择static、dynamic、guided、runtime
omp_set_num_threads(4);
//int sum =0;
#pragma omp parallel for schedule(static,4)
for(int i=0;i<16;i++){
//sum+=i;
printf("thread is:%d,thread sum is:%d\n",omp_get_thread_num(),i);
fflush(stdout);
}
/*
输出
thread is:2,thread sum is:8
thread is:2,thread sum is:9
thread is:1,thread sum is:4
thread is:1,thread sum is:5
thread is:1,thread sum is:6
thread is:1,thread sum is:7
thread is:2,thread sum is:10
thread is:2,thread sum is:11
thread is:3,thread sum is:12
thread is:3,thread sum is:13
thread is:3,thread sum is:14
thread is:3,thread sum is:15
thread is:0,thread sum is:0
thread is:0,thread sum is:1
thread is:0,thread sum is:2
thread is:0,thread sum is:3
*/
//dynamic
omp_set_num_threads(4);
//int sum =0;
#pragma omp parallel for schedule(dynamic,4)
for(int i=0;i<16;i++){
//sum+=i;
printf("thread is:%d,thread sum is:%d\n",omp_get_thread_num(),i);
fflush(stdout);
}
/*输出
thread is:2,thread sum is:4
thread is:2,thread sum is:5
thread is:3,thread sum is:12
thread is:2,thread sum is:6
thread is:2,thread sum is:7
thread is:0,thread sum is:0
thread is:0,thread sum is:1
thread is:0,thread sum is:2
thread is:0,thread sum is:3
thread is:3,thread sum is:13
thread is:3,thread sum is:14
thread is:3,thread sum is:15
thread is:1,thread sum is:8
thread is:1,thread sum is:9
thread is:1,thread sum is:10
thread is:1,thread sum is:11
*/
从以上两个输出呀我们可以看出,static的线程id和输出的值我们是可以预知的,但是dynamic的输出值和线程id不是对应的,多次输出可以看出来,是动态分配的。
ordered
ordered指令顾名思义,可以让循环代码中的某些代码执行按照一定的顺序来执行。下面的实例告诉我们了ordered是怎么工作的。
#pragma omp parallel for ordered
for(int i=0;i<20;i++){
//#pragma omp ordered
{
cout<<"thread num is:"<<omp_get_thread_num();
}
cout<<",i:"<<i<<endl;
}
/*
可以看出ordered下for循环按照顺序执行,线程也是按照顺序执行,等价于用不同的线程去串行执行一段代码
输出结果:
thread num is:0,i:0
thread num is:0,i:1
thread num is:0,i:2
thread num is:1,i:3
thread num is:1,i:4
thread num is:1,i:5
thread num is:2,i:6
thread num is:2,i:7
thread num is:2,i:8
thread num is:3,i:9
thread num is:3,i:10
thread num is:3,i:11
thread num is:4,i:12
thread num is:4,i:13
thread num is:5,i:14
thread num is:5,i:15
thread num is:6,i:16
thread num is:6,i:17
thread num is:7,i:18
thread num is:7,i:19
*/
private
表明某个变量是私有的
#pragma omp parallel private(name)
//代码是循环依赖的代码
上面有跑例子的
firstprivate
表明某个变量线程私有,但是在进入线程之前先给私有变量赋全局变量的初值
//用法
#pragma omp parallel firstprivate(name)
//例子
int sum = 0;
#pragma omp parallel for firstprivate(sum)
for(int i=0;i<20;i++){
sum+=i;
printf("thread num is:%d,sum is:%d\n",omp_get_thread_num(),sum);
//cout<<"thread num is:"<<omp_get_thread_num()<<",sum is:"<<sum<<endl;
}
cout<<"real sum is:"<<sum<<endl;
/*下面是输出结果,这里可以看出不同线程计算得到的sum是不一样的,
并且不影响全局变量sum 的值。sum在最开始都初始化为0,输出的是加上i之后的值。
*/
/*
thread num is:0,sum is:0
thread num is:0,sum is:1
thread num is:0,sum is:3
thread num is:0,sum is:6
thread num is:0,sum is:10
thread num is:3,sum is:15
thread num is:3,sum is:31
thread num is:3,sum is:48
thread num is:3,sum is:66
thread num is:3,sum is:85
thread num is:2,sum is:10
thread num is:2,sum is:21
thread num is:2,sum is:33
thread num is:2,sum is:46
thread num is:1,sum is:5
thread num is:1,sum is:11
thread num is:1,sum is:18
thread num is:2,sum is:60
thread num is:1,sum is:26
thread num is:1,sum is:35
real sum is:0
*/
lastprivate
表明某个变量线程私有,但是在线程结束之后将最后一个section的值赋给全局变量,本来也没闹清楚,后来实验了一下,大致清楚了,因为不同section之间计算的结果不一样嘛,就是最后执行完运算的那个section把值赋给全局变量
#pragma omp parallel lastprivate(name)
/*
下面是具体实验
可以看到,全局的sum输出的其实是thread3最后的结果,因为thread3到最后执行的时候sum=8,i=9,加起来就是17了,所以在最后取最后一个thread计算的结果赋值给全局变量sum。
*/
int sum = 0;
#pragma omp parallel for lastprivate(sum)
for(int i=0;i<10;i++){
printf("thread:%d,sum:%d\n",omp_get_thread_num(),sum);
sum +=i;
}
cout<<sum<<endl;
/*
thread:2,sum:0
thread:2,sum:6
thread:1,sum:0
thread:1,sum:3
thread:1,sum:7
thread:3,sum:0
thread:3,sum:8
thread:0,sum:32767
thread:0,sum:32767
thread:0,sum:32768
17*/
shared
表明某个变量是线程之间共享的.
#pragma omp parallel shared(name)
//这个默认就是shared的,不需要代码实验
default
要么是shared,要么是private.
#pragma omp parallel default(shared)
//一样不需要代码实验
reduction
通过operator规约,避免数据竞争,表明某个变量私有,在线程结束后加到全局变量上去,reduction每个线程创建变量的副本,按照operator进行初始化值,然后在线程结束的时候都加到全局变量之上。
#pragma omp parallel reduction(operation:name)
//实验在上面循环依赖的代码那里
int res = 10;
#pragma omp parallel for reduction(+:res)
for(int i=0;i<10000;i++){
res = res + i;
}
/*
这个程序会创建n个线程(默认取决与你的cpu数量),
每个线程里的res被初始化为0并且线程私有,
当每个线程都计算完成之后,将自己线程内的res值加到全局的res上去。
最后跟不parallel的结果一样,但是实现过程不一样。
*/
在循环次数比较多的情况下,会发生数据竞争(因为默认在并行体外定义的变量是shared的,在里面定义的是private的,shared的数据会由于在并行体内被修改而影响其他线程的赋值,所以会发生数据竞争),所以通过reduction规约,可以避免这种情况发生。
operator对应的初始化值的表(表格是网上找的):
操作 | 操作符 | 初始值 |
---|---|---|
加法 | + | 0 |
乘法 | * | 1 |
减法 | - | 0 |
逻辑与 | && | 0 |
逻辑或 | || | 0 |
按位与 | & | 1 |
按位或 | | | 0 |
按位异或 | ^ | 0 |
相等 | true | |
不等 | false | |
最大值 | max | 最小负值 |
最小值 | min | 最大正值 |
实验部分
由于我随便跑了一跑高斯模糊算法的手动实现(之前用python)写过,这次由于学了OpenMP这个并行编程的东西,打算跑一跑看看到底能提高多快(我的算法是最原始的算法),我的算法需要有opencv库(虽然opencv中已经有GaussianBLur函数,而且速度很快),但我主要想弄清楚高斯模糊算法到底怎么回事才用这么复杂的矩阵计算做的。
在qt下需要在pro文件中加入以下配置,这里由于我用到了opencv,所以还需要加入这样的配置:
QMAKE_CXXFLAGS+= -fopenmp
LIBS+= -lgomp -lpthread
INCLUDEPATH+=/usr/local/include\
/usr/local/include/opencv\
/usr/local/include/opencv2
LIBS+=/usr/local/lib/libopencv_highgui.so\
/usr/local/lib/libopencv_core.so\
/usr/local/lib/libopencv_imgproc.so\
/usr/local/lib/libopencv_imgcodecs.so
下面是parallel和不parallel的结果对比:
#include "mainwindow.h"
#include <QApplication>
#include "opencv2/opencv.hpp"
#include <omp.h>
#include <vector>
#include <time.h>
#include <math.h>
#define PI 3.1415926
using namespace cv;
using namespace std;
Mat getWeight(int r=3){
int l = r*2+1;
float sum=0;
Mat temp(l,l,CV_32FC1);
//
#pragma omp parallel for reduction(+:sum)
for(int i=0;i<temp.rows;i++){
for(int j=0;j<temp.cols;j++){
temp.at<float>(i,j) = 0.5/(PI*r*r)*exp(-((i-l/2.0)*(i-l/2.0) + (j-l/2.0)*(j-l/2.0))/2.0/double(r)/double(r));
sum = sum+ temp.at<float>(i,j);
//cout<<sum<<endl;
}
}
return temp/sum;
}
float Matrix_sum(Mat src){
float temp=0;
for(int i=0;i<src.rows;i++){
for(int j=0;j<src.cols;j++){
temp +=src.at<float>(i,j);
}
}
return temp;
}
Mat GaussianBlur_parallel(Mat src,Mat weight,int r=3){
Mat out(src.size(),CV_32FC1);
Mat temp;
int rows = src.rows-r;//shared
int cols = src.cols-r;//shared
#pragma omp parallel for
for(int i=r;i<rows;i++){
for(int j=r;j<cols;j++){
Mat temp(src,Range(i-r,i+r+1),Range(j-r,j+r+1));//int to float32
temp.convertTo(temp,CV_32FC1);
out.at<float>(i,j) = Matrix_sum(temp.mul(weight));
//cout<<"thread num is:"<<omp_get_num_threads()<<endl;
}
}
out.convertTo(out,CV_8U);
return out;
}
Mat GaussianBlur_normal(Mat src,Mat weight,int r=3){
Mat out(src.size(),CV_32FC1);
for(int i=r;i<src.rows-r;i++){
for(int j=r;j<src.cols-r;j++){
Mat temp(src,Range(i-r,i+r+1),Range(j-r,j+r+1));
temp.convertTo(temp,CV_32FC1);
//cout<<Matrix_sum(temp.mul(weight))<<endl;
//cout<<"("<<i-r<<","<<i+r+1<<")"<<","<<"("<<j-r<<","<<j+r+1<<")"<<"raw:"<<src.size()<<endl;
out.at<float>(i,j) = Matrix_sum(temp.mul(weight));
}
}
out.convertTo(out,CV_8U);
return out;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
omp_set_num_threads(8);
vector<Mat> split_img,out_img;
Mat src = imread("/home/xueaoru/projects/srm/car.jpg"),normal_img,parallel_img;
Mat weight = getWeight();
split(src,split_img);
out_img.clear();
double start_time = (double)getTickCount(),end_time;
for(auto s:split_img){
out_img.push_back(GaussianBlur_normal(s,weight));
}
end_time = (double)getTickCount();
cout<<"Normal compute time:"<<(end_time-start_time)*1000/getTickFrequency()<<"ms"<<endl;
merge(out_img,normal_img);
imshow("normal GaussianBlur",normal_img);
out_img.clear();
start_time = (double)getTickCount();
for(auto s:split_img){
out_img.push_back(GaussianBlur_parallel(s,weight));
}
end_time = (double)getTickCount();
cout<<"parallel compute time:"<<(end_time-start_time)*1000/getTickFrequency()<<"ms"<<endl;
merge(out_img,parallel_img);
imshow("parallel GaussianBlur",parallel_img);
imshow("raw",src);
return a.exec();
}
/*
输出结果如下:
Normal compute time:1609.72ms
parallel compute time:871.309ms
*/
[OpenMP] 并行计算入门的更多相关文章
- 在fortran下进行openmp并行计算编程
最近写水动力的程序,体系太大,必须用并行才能算的动,无奈只好找了并行编程的资料学习了.我想我没有必要在博客里开一个什么并行编程的教程之类,因为网上到处都是,我就随手记点重要的笔记吧.这里主要是open ...
- openmp并行计算
#include <omp.h>#include <stdio.h>#include <stdlib.h> void test(int n){ for (int i ...
- 并行计算之OpenMP中的任务调度
本文参考<OpenMP中的任务调度>博文,主要讲的是OpenMP中的schedule子句用法. 一.应用需求 在OpenMP并行计算中,任务调度主要用于并行的for循环.当for循环中每次 ...
- OpenMP 入门
OpenMP 入门 简介 OpenMP 一个非常易用的共享内存的并行编程框架,它提供了一些非常简单易用的API,让编程人员从复杂的并发编程当中释放出来,专注于具体功能的实现.openmp 主要是通过编 ...
- 通过 GCC 学习 OpenMP 框架
OpenMP 框架是使用 C.C++ 和 Fortran 进行并发编程的一种强大方法.GNU Compiler Collection (GCC) V4.4.7 支持 OpenMP 3.0 标准,而 ...
- openMP的一点使用经验【非原创】
按照百科上说的,针对于openmp的编程,最简单的就是在开头加个#include<omp.h>,然后在后面的for上加一行#pragma omp parallel for即可,下面的是较为 ...
- CentOS6中OpenMP的运行时间或运行性能分析
OpenMp作为单机多核心共享内存并行编程的开发工具,具有编码简洁等,容易上手等特点. 关于OpenMP的入门,博主饮水思源(见参考资料)有了深入浅出,循序渐进的分析.做并行开发,做性能分析是永远逃避 ...
- 并行计算之OpenMP入门简介
在上一篇文章中介绍了并行计算的基础概念,也顺便介绍了OpenMP. OpenMp提供了对于并行描述的高层抽象,降低了并行编程的难度和复杂度,这样程序员可以把更多的精力投入到并行算法本身,而非其具体实现 ...
- OpenMP 入门教程
前两天(其实是几个月以前了)看到了代码中有 #pragma omp parallel for 一段,感觉好像是 OpenMP,以前看到并行化的东西都是直接躲开,既然躲不开了,不妨研究一下: OpenM ...
随机推荐
- css hack汇总
注意点: 网上很多资料中常常把!important也作为一个hack手段,其实这是一个误区.!important常常被我们用来更改样式,而不是兼容hack.造成这个误区的原因是IE6在某些情况下不主动 ...
- 使用Xilinx SDSoc在Xilinx zcu102开发板上编程HelloWorld
关于Xilinx SDSoc的介绍我就不再复述了,我理解的也不一定准确,可以阅读官方文档了解SDSoc,你可以把它理解为一个集成开发环境 (IDE),通过SDSoc我们能够简单快速的对Xilinx的开 ...
- HDU 1506【单调栈】
思路: 转化成对于某一位置为最小值求向两边最远>=他的位置,用单调栈就能轻易完成. 那么ans=(left+right)*h[i]; 维护单调递增还是递减呢? 我们能很快反应到,一旦碰到一个比他 ...
- Unity 5着色器系统代码介绍(下)
http://forum.china.unity3d.com/thread-25738-1-10.html 上一篇对着色器系统的工作原理做了介绍,现在我们将继续深入,将目光聚焦在标准着色器的光照函数. ...
- 3dmax沿立方体边扩展出面
这个做法有问题,接缝问题处理起来很麻烦,立方体与平面的拼接基本就做不到 做建筑时,一般先做墙,然后通过墙扩展出地表,这么做可以保证墙和地表是一体的,避免产生缝隙 1 新建2个Cube 2 转换为可编辑 ...
- codevs1074 食物链
1074 食物链 2001年NOI全国竞赛 时间限制: 3 s 空间限制: 64000 KB 题目等级 : 钻石 Diamond 题解
- mac for Idea配置Tomcat
参考链接地址:http://blog.csdn.net/zsy3313422/article/details/52583091 IntelliJ使用 ##使用IntelliJ IDEA配置web项目时 ...
- 洛谷P2136 拉近距离
题目背景 我是源点,你是终点.我们之间有负权环. --小明 题目描述 在小明和小红的生活中,有\(N\)个关键的节点.有\(M\)个事件,记为一个三元组\((S_i,T_i,W_i)\),表示从节点\ ...
- Codeforces 126B(kmp)
要点 头尾的最长相同只要一个kmp即可得,于是处理中间部分 扫一遍记录一下前缀的每个位置是否存在一个中间串跟它相同,见代码 如果当前没有,接着用Next数组去一找即可 #include <cst ...
- NET Core开发
NET Core开发 Visual Studio 2017 ASP.NET Core开发,Visual Studio 2017 已经内置ASP.NET Core 开发工具. 在选择.NET Core ...