다음으로 Chapter 4를 정리해 봅니다.

 

 

 

 

 

Chapter 4.1 지역 변수, 범위, 지속시간

 

 

이번 챕터는 변수의 범위에 대한 내용들입니다.

 

namespace work1
{
	int a = 1;
	void doSomething()
	{
		a += 3;
	}
}

namespace work2
{
	int a = 1;
	// 이름은 같은데 하는 일이 다른 경우(파라미터까지 다 같음)
	// 이름이 같은데 파라미터가 다른 경우는 다른 함수로 인식해서 다른 함수로 인식.
	void doSomething(int b)
	{
		a += 5;
	}
}



int main()
{
	using namespace std;

	int apple = 5;

	cout << apple << endl;
	// apple을 선언한 이후로는 apple을 볼 수 있다. scope를 알 수 있음.
	// main문 밖에서는 사용할 수 없다.
	// 중괄호가 끝나면 메모리가 이미 반납되어서 apple이라는 변수는 없다.
	// apple = 1;

	// nested block
	// 밖에서 정의된 변수는 block 안쪽에서 볼 수도 있고 사용할 수도 있다.

	{
		// 만약 int apple = 1;로 정의하면 밖에 있는 apple하고 다름.
		// 이름이 같더라도 더 적은 영역에 있으면 기존 apple이 사라짐 (name hiding), 이름은 같지만 완전히 다름.
		// 따라서 가급적 변수 이름은 범위가 다르더라도 다르게 짓는 것이 좋음.
		apple = 1;
		cout << apple << endl;
	}

	// 현대적 프로그래밍에서는 변수가 살아남는 범위를 줄이려고 함.
	// 따라서 변수가 사용되는 곳 근처에서만 살아남도록 최소한의 범위를 갖도록 블럭으로 만들어줌.
	// 범위를 쪼개고 쪼개는게 객체지향 프로그래밍의 기본적인 철학.

	cout << apple << endl;


	// :: 연산자는 영역/범위 결정 연산자. Scope resolution operator
	// 이름이 충돌할 때 이를 해결하기 위해서 이름 공간을 쪼갠다.
	// C++17 에서는 nested namespace 지원함.
	work1::a;
	work1::doSomething();

	work2::a;
	work2::doSomething(3);
    
}

 

주석으로 내용을 정리해 두었는데, 글로 한 번만 더 정리해 보자면,

 

- 중괄호가 끝나면 메모리가 반납되며 변수는 제거됨. (변수의 범위와 지속시간)

- block 밖에서 정의된 변수는 block 안쪽에서 볼 수도 있고 사용도 가능.

- 만약 block 안에서 같은 자료형과 같은 변수명으로 다시 한번 선언하는 경우, block 밖에 있던 변수와는 별개.

- 범위가 다르더라도 혼동을 줄이기 위해 변수명은 다르게 지어야 한다.

- 변수 이름으로 충돌하는 경우를 해결하기 위해 namespace 기능을 사용함. 

 

 

 

 

Chapter 4.2 전역 변수, 정적 변수, 내부 연결, 외부 연결

 

 

- 지역 변수(Local variable): Linkage가 없으며, Linking 시 고려하지 않음.

- 내부 연결(Internal linkage): 한 파일 안에서는 어디서든 사용할 수 있음.

- 외부 연결(External linkage): cpp 파일이 여러 개 있을 때, 한 cpp 파일에서 선언한 변수를 다른 cpp에서도 사용할 수 있음.

 

 

예시 코드 1

int value = 123;

int main()
{

	std::cout << value << std::endl;
	
	int value = 1;
	
	std::cout << value << std::endl;

	// Global scope operator
	std::cout << ::value << std::endl; // 영역 연산자, 다른 영역에 정의된 변수를 사용하는 것. 
}

 

 

 

 

main문 안에서 value가 선언되기 전에 cout으로 호출하게 되면 밖에서 이미 선언된 변수의 값을 가져온다.

 

main문 안에서 value가 선언된 후에는 cout으로 호출했을 때 내부에 선언된 변수 값을 가져온다.

 

만약 밖에서 선언된 변수의 값을 출력하고 싶다면, ::를 활용한다. 

 

 

 

예시 코드 2

void doSomething()
{
	// static? c를 만든 사람 입장에서 봐야한다... 
	// 변수 a가 os로부터 받은 메모리가 static이다. 
	// 이 영역 안에 변수가 선언될 때 같은 메모리를 사용한다. 두번, 세번 실행되더라도 같은 메모리 사용. 초기화를 한번만 함.
	// static 변수는 반드시 한 번은 초기화를 해줘야함. (메모리에 얹어줘야하기 때문에)
	// 함수를 몇 번 호출되는지 확인해볼 때 사용. 디버깅에서 쓴다.
	static int static_a = 1;
	int a = 1; // 계속 메모리를 새로 할당받음. 

	a++;
	static_a++;

	std::cout << "a: " << a << std::endl;

	std::cout << "static_a: " << static_a << std::endl;
}

int main()
{
	doSomething();
	doSomething();
	doSomething();
	doSomething();
}

 

 

 

다음은 static 변수에 대한 설명이다.

 

static이라는 것은 os로부터 받은 메모리가 static이라는 얘기이다. 즉, 이 영역 안에서 변수가 선언될 때 같은 메모리를 사용한다. 2번, 3번 실행되더라도 같은 메모리를 사용하게 되며, 초기화는 한 번만 한다.

 

따라서 static 변수는 반드시 한 번은 초기화를 해줘야한다는 특징이 있다.

 

반면 static int가 아닌, 일반 int를 사용하면 계속 초기화를 해주기 때문에 값이 같은 것을 확인할 수 있다.

 

 

 

 

 

 

여러 cpp 파일에서 사용할 수 있는 방법은 바로 extern을 붙이는 것이다.

 

test_cpp.cpp라는 파일에서 선언된 int e_a는 Chapter 4_2.cpp라는 파일에서도 사용할 수 있는데, 이는 extern int e_a로 선언했기 때문이다.

 

함수 또한 다른 cpp 파일에서 정의된 것을 사용할 수 있는데, 이는 전방선언을 활용하면 된다. 단 함수의 경우 extern이 필수는 아니다.

 

 

 

만약 내가 여러 cpp 파일에서 사용할 상수가 있다고 가정하자.

 

그럼 어떻게 해야 효율적으로 상수를 정의해서 사용할 수 있을까?

 

 

// my_constant.h
namespace Constants
{
	extern const double pi = 3.14;
	extern const double gravity = 9.8;
}

// test_1.cpp
#include "my_constant.h"

int main() {
    std::cout << &Constants::pi << std::endl;
    return 0;
}

// test_2.cpp
#include "my_constant.h"

int main() {
    std::cout << &Constants::pi << std::endl;
    return 0;
}

 

위 코드처럼, 만약 헤더파일에 extern const 변수를 선언하게 되면, 각각의 cpp에서 해당 변수를 사용할 때 메모리가 각각 사용된다.

 

안타깝게도 강의에서는 자세히 언급을 해주시지 않아서, 제미나이를 활용해 보았더니 다음과 같은 대답을 얻을 수 있었다.

 

 

 

 

헤더 파일에 extern const로 변수를 선언하는 경우, 컴파일러에 의해서 번역될 때 각 cpp 파일에 별도의 변수 인스턴스를 만들도록 지시한다고 한다.

 

따라서 이를 방지하려면, 헤더 파일에서는 변수 선언만 하고, 값을 선언하는건 별도의 cpp 파일을 만들어서 해줘야 한다.

 

 

// Constants.h
namespace Constants
{
    extern const double pi;
    extern const double gravity;
}

// Constants.cpp
#include "Constants.h"
namespace Constants
{
    const double pi = 3.14;
    const double gravity = 9.8;
}

// test_1.cpp
#include "Constants.h"
#include <iostream>

int main() {
    std::cout << &Constants::pi << std::endl;
    return 0;
}

// test_2.cpp
#include "Constants.h"
#include <iostream>

int main() {
    std::cout << &Constants::pi << std::endl;
    return 0;
}

 

다음과 같이, 별도의 cpp 파일에서 값을 제공해주는 방식으로 하면 다른 cpp 파일에서 해당 변수 사용 시 주소가 같다.

 

 

 

 

 

 

Chatper 4.3 Using문과 모호성

 

 

 

예시 코드 1

namespace a
{
	int my_var(10);
}

namespace b
{
	int my_var(20);
}

int main()
{

	using namespace a;
	using namespace b;

	std::cout << my_var << std::endl; // 모호하다는 에러 발생
    
    std::cout << a::my_var << std::endl;
    std::cout << b::my_var << std::endl;
}

 

 

namespace a와 namespace b 모두에 my_var라는 int 변수가 있는 경우, using namespace a, b 모두를 사용하면 my_var이 a에 있는 걸 써야 할지, b에 있는 걸 써야 할지 몰라서 모호하다는 에러가 발생한다.

 

가장 명확하게 해결하는 방법은, :: 연산자를 활용해 어떤 namespace에 있는 변수인지를 명시적으로 알려주는 것이다.

 

 

 

예시 코드 2

namespace a
{
	int my_var(10);
}

namespace b
{
	int my_var(20);
}


int main()
{

	using namespace b;
    
	{
		using namespace a;
		std::cout << my_var << std::endl; // a와 b 영향을 다 받는다, 모호함 해결 X
	}

	std::cout << my_var << std::endl; // namespace b로 사용됨.
}

 

블록으로 따로 영역을 잡아주고, using namespace a를 적용해주는 경우, 블록 안에 있는 my_var는 여전히 using이 두 개 적용 되는 상황이므로 모호성이 해결되지 않는다.

 

단, 블록 바깥쪽에 있는 my_var는 b의 my_var로 적용된다.

 

 

예시 코드 3

namespace a
{
	int my_var(10);
}

namespace b
{
	int my_var(20);
}


int main()
{

	
    
	{
		using namespace a;
		std::cout << my_var << std::endl; 
	}

	{
		using namespace b;
		std::cout << my_var << std::endl; 
	}
}

 

블록으로 영역을 따로 따로 잡아주면, 모호성이 없어진다.

 

 

using namespace를 특정 헤더에서 전역 범위에 넣어버리면, 헤더를 include 하는 모든 cpp 파일에 영향을 주게 된다.

 

따라서 가급적이면 cpp 파일에 넣는 방향이 좋다. 

 

 

using namespace std처럼 큰 범위에 적용하는 using을 사용하는 경우, 다른 namespace에서 std에 들어간 함수명과 겹치는 이름의 변수를 사용하려고 할 때 선택을 해야 한다. using namespace std를 포기할지, 혹은 using namespace a를 포기할지. 강의에서는 count라는 변수를 통해서 예시를 보여주셨는데, std::count라는 함수가 있어서 namespace에 count라는 이름으로 변수를 사용하게 되면 겹치게 되어 문제가 발생하는 것을 확인할 수 있었다.

 

 

 

 

Chapter 4.4 auto 키워드와 자료형 추론

 

 

 

예시 코드 1

// Parameter에는 auto 사용 불가.
auto add(int x, int y) -> int
{
	return x + y;
}



int main()
{
	using namespace std;

	// 자료형을 상황에 따라서 결정하게 만드는 것을 형 추론이라고 한다. auto 키워드 사용.
	// 값을 주지 않으면 사용할 수 없음.
	// 계산 결과가 어떻게 나오는지 인지하고 있어야 함.

	auto a(123); // int
	auto d(123.0); // double
	auto c(1 + 2.0); // double

	// 함수의 return 값에도 auto 사용 가능.
	auto result = add(1, 2);
}

 

 

자료형을 상황에 따라 결정하게 만드는 것을 형 추론이라고 하는데, 이는 auto 키워드를 사용한다.

 

예를 들어서, auto a(123); 라고 하면 값이 int 이므로 a는 int로 결정이 된다.

 

auto d(123.0); 라고 하면 값이 소수점이므로 d는 double로 결정이 된다.

 

함수의 return type에도 auto를 사용할 수 있고, 함수의 return을 받는 변수도 auto를 사용할 수 있다.

 

함수의 return type을 auto로 하더라도, 명시적으로 자료형을 표기하고 싶다면 ->를 사용해서 표현할 수 있다.

 

auto func() -> int 로 만드나, int func()로 만드나 같지만, 여러 함수가 있을 때 각 함수의 return type을 한눈에 보기에는 전자를 사용하는 것이 더 좋다.

 

 

 

Chapter 4.5 형변환 (Type conversion)

 

 

 

예시 코드 1

#include <iostream>
#include <typeinfo>


cout << typeid(4).name() << endl;
cout << typeid(a).name() << endl;

 

typeinfo를 include하면, typeid라는 함수를 사용할 수 있는데, 이는 자료형을 알려주는 함수이다.

 

auto를 사용하는 경우라던가, 변수의 자료형을 알 수 없는 경우 유용하게 사용할 수 있다.

 

 

 

예시 코드 2

// 메모리를 작게 사용하는 데이터 타입을 메모리를 크게 사용하는 데이터 타입으로, 
// numeric promotion, 암시적 형변환 사용
float a = 1.0f;
double d = a;

 

다음으로는 numeric promotion에 대한 설명이다.

2

메모리를 작게 사용하는 데이터 타입(float)를 메모리를 크게 사용하는 데이터 타입(double)으로 변경하는 경우를 말한다. 이런 경우는 명시적인 형변환이 아닌, 암시적인 형변환을 사용한다는 특징이 있다.

 

적은 메모리에서 큰 메모리로 형변환이 되기 때문에, 값의 변경 없이 type만 변경된다.

 

 

 

예시 코드 3

// numeric conversion
double dd = 3; // int를 double로, 타입이 바뀌는 경우
short s = 2; // int를 short로, 큰 자료형을 작은 자료형으로 보내는 경우

// numeric conversion issue
int i = 30000;
char c = i;

// issue 2
double d_ = 0.123456789;
float f_ = d_;

// unsigned라서 제대로 저장될 수 없는 경우
// 자료형의 우선순위가 int가 가장 낮고, unsigned int,
// long, unsigned long, long long, unsigned long long, float, double, long double 순이다.
// unsigned가 우선순위가 더 높으니 int로 바꾸지 않는 경우임. (주의!)
cout << 5u - 10u;

 

다음은 numeric conversion에 대한 설명이다.

 

int를 double로 바꾸는 것 처럼, data type 자체가 변하는 경우가 있으며

 

int를 short로, 즉 큰 자료형을 작은 자료형으로 보내는 경우가 있다.

 

 

 

이런 경우에 문제가 발생할 수 있는데, 예를 들어 int i = 30000;로 정의했는데 char 타입으로 받게 되면 char 타입의 자료형 크기를 초과하므로 c의 값은 정상적으로 i 값을 받아올 수 없다.

 

마찬가지로 double 자료형의 변수를 float로 받는 경우, 정밀도의 차이가 있어 double 변수의 값을 온전히 float 변수가 받을 수 없다.

 

그리고 자료형의 우선순위 문제로, 5u(숫자 5이나, unsigned int를 의미함) - 10u는 실제로 -5 값으로 나와야 하나, 우선순위 상 unsigned int가 더 높기 때문에 이를 signed int 형태인 -5로 표현하지 않고 이상한 값으로 표현하게 된다. 이런 경우 우리가 기대하는 값과 전혀 다른 값이 들어갈 수 있기 때문에 주의해야 한다. 

 

 

 

 

Chatper 4.6 문자열 std string 소개

 

 

해당 강의에서는 문자열을 표현하는 자료형인 std::string에 대해서 소개한다.

 

 

예시 코드 1

#include <string>

int main()
{

	// char하고 string하고 색이 다르다.
	// C++에서 기본적으로 제공해주는건 한 글자다.
	// 한 글자를 여러개 나열하는 방식으로 문자열을 사용한다.
	// char가 기본적으로 사용하는 방식
	const char my_strs [] = "Hello, World";

	// 프로그래머들이 편하라고 제공하는 방식.
	// 프로그래머들이 문자를 다룰 때 많이 사용하는 것들을 string 안에 미리 구현해뒀음.
	// 일종의 사용자 정의 데이터 타입이라고 보면 됨. 기본 자료형처럼 쓸 수 있는데 추가적인 기능이 들어가있는 것.
	const string my_hello = "Hello, World";

	string my_ID = "123";

	cout << my_strs << endl;

 

 

C++에서 제공하는 기본적인 자료형은 char이나, 문자를 조금 더 편하게 다룰 수 있도록 만들어진 것이 std::string 자료형이라고 이해하면 된다고 한다. 

 

그렇다 보니, const char는 글자색이 같은데, string은 색이 다르다. string은 기본 자료형이 아니며, 일종의 사용자 정의 데이터 타입이라고 볼 수 있다. 

 

 

예시 코드 2

cout << "Your name ? : ";
string name;

//cin >> name;
std::getline(std::cin, name);

cout << "Your age ? : ";
string age;

std::getline(std::cin, age);

cout << "My name is: " << name << " My age is : " << age << endl;

 

일반적으로 데이터를 입력받는다고 하면 cin을 사용하면 된다. 

 

그러나, cin을 사용할 때 한 칸을 띄어버리게 되면 두 개의 cin으로 나눠서 들어가게 된다.

 

예를 들어서, 

 

 

 

cin을 사용할 때는 한 칸을 띄우게 되면 happy는 앞 cin에, cat은 그다음 cin으로 들어가게 되어서 두 번째 cin에는 입력을 할 수 없으며 자동적으로 할당을 하게 된다.

 

따라서 이를 방지하려면 std::getline을 사용하여 cin을 받아야한다. 

 

 

 

예시 코드 3

cout << "Your age ? : ";
int age;

cin >> age;

// \n이 만날 때 까지 글자를 무시하라
//std::cin.ignore(std::numeric_limits<std::streamsize>::max() , '\n');

cout << "Your name ? : ";
string name;

std::getline(std::cin, name);

cout << "My name is: " << name << " My age is : " << age << endl;

 

이번에는 이전과 달리, int를 입력받고 string을 입력 받으려고 하는 경우이다.

 

int를 입력 받으려면 cin을 사용해야 하므로, cin으로 age를 받고 다음은 문자열을 받기 위해 getline을 사용하려고 하였다.

 

하지만 cin을 사용해서 입력한 후 엔터 키를 누르면 입력 버퍼에 \n가 남아있어서 이를 getline에서 읽고 빈 문자열을 name에 할당한 후 종료하게 되는 문제가 있다.

 

따라서 이를 해결하려면 cin.ignore를 적용하여 글자를 무시하도록 적용해줘야 한다.

 

 

예시 코드 4

string a = "Hello, "; // 마우스를 대보면 [8]로 나오는데, 이는 문자열이 8개라는 의미. 하지만 실제로는 7개, 맨 마지막에 널 캐릭터가 숨어 있음.
string b = "World";
string hw = a + b;

hw += " Good";

//cout << hw << endl;
cout << hw.length() << endl; // string 길이 확인.

 

다음은 string을 + 연산자를 활용해서 붙이는 경우를 보여준다.

string a 하고 string b를 + 연산자로 붙이면, 그대로 문자열이 붙게 된다.

 

그리고 문자열의 길이를 확인할 때는 .length()를 활용하면 된다.

 

조금 특이한 점은, string a의 경우 "Hello, "이니까 실제로는 7글자인데, 마우스 커서를 대보면 const char [8]로 나온다. 이는 맨 마지막에 널 캐릭터가 숨어져 있기 때문이며, C 스타일 문자열은 항상 널 캐릭터로 끝나야 하는 규칙이 있다. 이를 통해 문자열의 끝을 확인할 수 있는 것이다.

 

 

 

Chatper 4.7 열거형 (enumerated types)

 

 

이번 강의에서는 enum 이라고 하는 자료형인 열거형에 대해서 다룬다.

 

// 사용자 정의 자료형 (user-defined data types)
enum Color
{
	COLOR_BLACK, // -3으로 지정나면 나머지들은 +1씩 값으로 지정됨.
	COLOR_RED,
	COLOR_BLUE,
	COLOR_GREEN,
};  // ; 없으면 빌드 에러남.

enum Feeling
{
	HAPPY,
	JOY,
	TIRED,
	BLUE
};

Color my_color = COLOR_RED;
int color_id = COLOR_RED;

 

일단 enum의 특징은, 처음 값을 0으로 시작해서 1씩 증가하도록 설계되어 있다. COLOR_BLACK이 0이고, COLOR_RED가 1이고 이런 식이다.

 

기본적으로는 그렇지만, 만약 수동으로 값을 지정해 주면, 값이 달라지나 규칙은 유지된다.

 

예를 들어 맨 처음 값을 -3으로 주면 그다음 값은 -2, -1, 0이 되는 것이다.

 

enum 사용 시 맨 마지막에 괄호 닫고 ;를 꼭 써줘야만 빌드에 문제가 없다.

 

그리고 다른 enum이더라도 만약 안에 사용하는 요소의 이름이 같으면 문제가 생긴다. 따라서 이름은 항상 다르게 지어주어야 한다.

 

 

COLOR_RED는 Color라는 이름의 자료형인 my_color에 사용할 수도 있고, int형 변수에도 사용할 수 있다. cout으로 출력하면 정상적으로 1로 출력이 된다.

 

그리고 여러 cpp 파일에서 enum을 공동으로 사용하는 경우가 많기 때문에, 헤더파일에 enum을 정의해 두고 각. cpp에서 #include 형식으로 헤더 파일을 읽어오게 만들어주면 유용하게 사용할 수 있다고 한다.

 

 

Chapter 4.8 영역 제한 열거형 (열거형 클래스)

 

 

해당 강의에서는 이전 강의에서 배웠던 열거형 자료형에서 변화한 형태인 열거형 클래스에 대해서 다루게 된다.

 

 

예시 코드 1

enum Color
{
    RED,
    BLUE
};

enum Fruit
{
    BANANA,
    APPLE
};

Color color = RED;
Fruit fruit = BANANA;

if (color == fruit)
{
    cout << "Color == fruit";
}

 

우선 저번 강의 때 배웠던 열거형 type을 그대로 사용해 보자.

 

열거형의 특성상 안에 있는 값들을 int로 사용한다.

 

따라서 Color 자료형의 RED와 Fruit 자료형의 BANANA는 동일하게 int로는 0이다.

 

그래서 if 문에서도 같다고 판단을 하게 된다.

 

이런 경우 int 값으로는 같으나 실제 데이터는 다른 상황이니, 실수할 가능성을 만들 수 있다.

 

이를 방지하고자 사용하는 것이 enum class이다.

 

 

 

예시 코드 2

using namespace std;

enum class Color
{
    RED,
    BLUE
};

enum class Fruit
{
    BANANA,
    APPLE
};

Color c_1 = Color::RED;
Color c_2 = Color::BLUE;

if (c_1 == c_2)
{
    cout << "same color! " << endl;
}

 

enum과 다른 점은, enum class에서 사용된 멤버는 enum 때처럼 그냥 사용할 수 없고, 마치 namepsace처럼 ::를 사용해야 한다.

 

그래서 기존하고 다르게 Color::RED 이런 식으로 사용해야 한다.

 

그리고 enum 때와 다르게 만약 Fruit 자료형 변수를 사용할 경우 == 연산자 자체를 사용할 수가 없다.

 

 

강의를 듣고 나서 약간 헷갈리는 부분들이 있어서, Gemini를 활용해 정리를 요청했더니 조금 더 깔끔하게 정리된 것 같다.

 

해당 내용을 첨부하니, 정리용으로 보면 좋을 듯하다.

 

 

 

enum을 수업할 때 언급된 내용도 있고 아닌 내용도 있는데 한번 정리해 보자면...

 

우선 Scope가 다른데, 이전 enum 수업에서도 언급이 나왔다시피 enum 안에 멤버로 선언된 변수는 다른 범위에서도 접근이 가능하다. 그래서 서로 다른 enum 끼리 같은 멤버를 쓰면 안 된다는 내용이 수업시간에 나왔었다. 마치 전역 변수처럼 사용되기 때문이다. 

 

그런데 enum class의 경우 namespace처럼 범위가 한정되기 때문에 이런 문제가 발생하지 않는다.

 

다음으로는 암시적 변환에 대한 부분인데... enum에 사용된 멤버는 암시적으로 형 변환이 된다. 특히 enum의 멤버는 int 값으로 저장이 되고 있기 때문에... 이를 int로 형변환 할 수 있다.

 

하지만 enum class의 경우 static_cast <int>를 통해서 명시적 형변환을 해줘야만 int로 변환이 가능하다. 이런 관점에서는 enum class가 훨씬 안전하다고 볼 수 있겠다.

 

그리고 자료형에 대해서는, enum의 경우 보통 int로 사용되나 enum class는 자료형을 명시적으로 지정할 수 있다고 한다. 이 내용은 수업시간에 나오진 않았던 내용인데 이런 기능을 보니 확실히 enum보다는 enum class가 더 많이 쓰이는 이유를 알 수 있을 듯하다.

 

namespace의 경우 이미 잘 아는 내용이고... 비교에 대해서도 앞의 예제에서 언급했듯이 다른 enum class 끼리는 비교 연산자가 작동하지 않는다. (에러가 발생함)

 

 

 

Chapter 4.9 자료형에게 가명 붙여주기

 

 

해당 강의에서는 자료형에 새로운 이름을 붙여서 사용하는 방식인 typedef에 대해서 다룬다.

 

 

#include <iostream>
#include <cstdint>
#include <vector>

int main()
{
	typedef double distance_t; // 코드상에서 둘 다 사용 가능함. 보통 뒤에는 _t는 타입 이름이다 라는 의미.

	double		my_distance;
	distance_t	home2work;
	distance_t  home2school;

	// aliases를 사용하는 이유에 가까움.
	//typedef std::vector<std::pair<std::string, int>> pairlist_t;
	using pairlist_t = std::vector<std::pair<std::string, int>>;

	pairlist_t pairlist1;
	pairlist_t pairlist2;




}

 

 

typedef의 경우 typedef 사용하고자 하는 자료형 별명 순서로 사용하게 된다

 

위 코드에서 distance_t라는 자료형은 실제로 double과 같은 자료형이나 이름만 붙여준 것이라고 보면 된다.

 

그리고 여러 자료형을 혼합해서 사용해 자료형의 이름이 길어질 때, 이를 축약해서 사용할 때도 유용하게 사용할 수 있다.

 

 

Chapter 4.10 구조체 struct

 

 

이번 강의에서는 클래스로 넘어가는 다리이자, 다양한 자료형을 하나의 바구니에 담을 수 있는 방법인 구조체에 대해서 다룬다.

 

 

예시 코드 1

#include <string>

struct Person
{
	double  height = 3.0;
	float	weight = 200.0;
	int		age = 100;
	string  name = "Hello";

	// 여러 변수들을 가지고 기능을 하려고 하는 함수다.
	// 데이터와 기능을 묶은 것.
	void print()
	{
		cout << height << " " << weight << " " << age << " " << name << " ";
		cout << endl;
	}
};

int main()
{
	Person me;
    cout << me.name << endl;
    
    Person me{ 2.0, 100.0, 20, "Jack Jack"};
    me.print();
}

 

구조체는 여러 자료형을 한 번에 담을 수 있는 특징을 가지고 있다.

 

따라서 struct 안에 double float int string 등 다양한 자료형이 존재할 수 있으며, 위 코드와 같이 초기값을 주기도 한다.

 

그리고 신기하게 struct 안에 함수를 쓸 수도 있다. 

 

구조체의 멤버 변수에 접근할 때는 . 을 사용하면 된다.

 

uniform initialization을 활용하면 조금 더 손쉽게 구조체에 값을 줄 수 있다.

 

 

 

예시 코드 2

struct Employee			// 14 bytes
{
	short	id;			// 2 bytes
	int		age;		// 4 bytes
	double	wage;		// 8 bytes
};

// 자료를 배치할 때 컴퓨터 잘 처리할 수 있는 형태로 하다보면 추가로 더 들어감. (패딩 이라고 부름)
Employee emp1;
cout << sizeof(emp1) << endl; // 16

 

구조체 type에도 sizeof를 사용할 수 있다.

 

다만, 자료를 배치할 때 컴퓨터가 잘 처리할 수 있는 형태로 하다 보면 실제 값하고는 차이가 있을 수 있다고 하니 주의해야 한다고 한다. 이를 패딩이라고 한다고 한다.

 

Gemini에게 물어보니, 조금 더 구체적인 답변을 들을 수 있었다.

 

 

 

수업 시간에도 2바이트를 처리하기가 좀 어렵다? 이런 얘기를 하셨는데, Gemini의 답변을 듣고 나니 CPU가 메모리에 접근할 때 4 byte나 8 byte와 같은 경계 기준이 있는 것으로 보인다.

 

 

 

 

 

여기까지 Chatper 4를 모두 정리해보았다.

 

 

중간에 회사 일이 너무 바빠 퇴근을 할 수 없어, 작성이 계속 지연되었다.

 

 

다시 열심히..! 해보려고 한다

 

 

 

 

순서상으로는 Chapter 0부터 해야 되지만, 지금 Chapter 3을 듣고 있어서... 우선 3부터 정리합니다.

 

 

현재 정리하고 있는 강의는 다음 강의입니다.

https://www.inflearn.com/course/following-c-plus

 

홍정모의 따라하며 배우는 C++ 강의 | 홍정모 - 인프런

홍정모 | , 6,000+명의 수강생이 증명합니다 👍모던 C++ 바이블, 따배씨++를 만나보세요! <2024 프로그래밍 공부 순서> [임베딩 영상]   뛰어난 프로그래밍 실력을 갖추고 싶다면. [사진] 모던(Modern) C+

www.inflearn.com

 

 

 

 

 

Chapter 3 목차

 

 

 

 

 

Chapter 3.1 연산자 우선순위와 결합 법칙

 

 

 

C++에서는 연산자 간의 우선순위가 있다. 여러 연산자가 나열되어 있는 경우, 우선순위가 높은 연산자를 먼저 실행하게 된다.

 

https://en.cppreference.com/w/cpp/language/operator_precedence

 

C++ Operator Precedence - cppreference.com

The following table lists the precedence and associativity of C++ operators. Operators are listed top to bottom, in descending precedence. a, b and c are operands. ↑ The operand of sizeof cannot be a C-style type cast: the expression sizeof (int) * p is

en.cppreference.com

 

사실 이 우선순위를 다 외우고 있기는 어려울 것 같고, 원하는 결과가 나오지 않는다면 표를 참고해서 문제를 해결해야 한다.

 

그리고 원하지 않는 결과가 나오는 것을 방지하려면, 괄호를 통해서 연산의 범위를 잘 정해주는 것이 위험을 줄이는 방법이다. (다른 개발자들도 우선순위를 모두 외우지는 못한다!)

 

연산자 우선순위가 적용되는 방향이 Left-to-right가 있고, Right-to-left가 있으니 이 또한 주의해야 한다.

 

우리가 수학에서 많이 쓰는 우선순위 (*, /가 +, -보다 더 우선순위가 높다)는 헷갈리지 않지만, 그렇지 않은 경우가 어려운 것 같다.

 

 

수업 코드 1

int a = 1;
int b = 2;
int c = 3;

a = b = c; // c -> b -> a로 assignment 됨.

std::cout << "a: " << a << " b: " << b << " c: " << c << std::endl; // a, b, c 모두 3

 

-> a = b = c로 해주는 경우 c를 b로 assignment 하고, b를 다시 a에 assignment 해서 결국 a, b, c가 같은 값이 되어 버린다.

 

 

수업 코드 2

int w = 3;
double t = 20;
t /= --w + 5; // --w + 5부터 진행하고, /=를 적용함.

std::cout << w << std::endl;

std::cout << "t: " << t << std::endl;

 

-> /= 연산자의 경우, right-to-left라서 우측부터 진행하고 좌측 진행.

 

따라서 w는 2가 되고(--w 영향), t는 2.85714 값이 나온다. 이는 t가 double이기 때문에, 결과가 소수점으로 출력된다.

 

 

 

 

Chapter 3.2 산술 연산자 (Arithmetic Operators)

 

 

몫 연산자(/)와 나머지 연산자(%)에 대해서 정리하는 챕터이다.

 

 

수업코드 1

int x = 7;
int y = 4;

// x / y ==> 몫, x % y ==> 나머지
// 나누기 시 한쪽이 실수면 결과가 실수로 나온다.
cout << "x / y : " << x / y << endl;
cout << "x % y : " << x % y << endl;
cout << "only x float case: " << float(x) / y << endl;
cout << "only y float case: " << x / (float)y << endl;
cout << "x and y float case: " << float(x) / float(y) << endl;

// 음의 정수의 경우, 소수점을 모두 절삭함. 
cout << "-5 / 2 ? " << -5 / 2 << endl;

// 왼쪽에 있는 숫자가 음수면 나머지의 결과도 음수다.
// 왼쪽에 있는 숫자가 양수면 나머지의 결과도 양수다.
cout << "-5 % 2 ? " << -5 % 2 << endl;
cout << "5 % 2 ? " << 5 % 2 << endl;

 

 

 / 연산자는 몫을 나타내고, %는 나머지를 나타낸다. 

 

피연산자들이 둘 다 int면 몫과 나머지 모두 int로 출력한다. 단, 피연산자 중 한 개라도 float이면 결과를 float로 출력한다.

 

음의 정수의 경우 몫 계산 시 소수점을 절삭하며, 나머지 계산 시 왼쪽에 있는 숫자의 부호에 맞춰서 결과가 나온다.

 

 

 

 

Chapter 3.3 증감 연산자 (Increment decrement operators)

 

 

증감 연산자의 위치에 따른 차이를 알려주는 챕터이다.

 

 

수업코드 1

int add(int a, int b)
{
	return a + b;
}

int x = 6, y = 6;

cout << x << " " << y << endl; // 6 6

// 앞에 +가 붙는 경우 x에 1을 더해주고 출력
// 뒤에 +가 붙는 경우 x를 stream에 보낸 다음에 1을 더해지는 것.
cout << ++x << " " << --y << endl; // 7 5

cout << x << " " << y << endl; // 7 5

cout << x++ << " " << y-- << endl; // 7 5

cout << x << " " << y << endl; // 8 4

int a = 1, b = 2;
int v = add(a, ++b);

cout << v << endl;

 

 

 

 

증감 연산자가 앞에 붙는 경우, 바로 해당 변수에 적용해 준다.

 

따라서 ++x를 출력할 때 기존 x 대비 +1이 된 것을 알 수 있다.

 

하지만 증감 연산자가 뒤에 붙는 경우, 해당 출력 코드가 종료된 이후에 적용된다.

 

따라서 x++를 출력할 때는 여전히 x의 값이 7이지만, 그다음 줄에서 x를 cout 했을 때 8이 되는 것을 알 수 있다.

 

 

그리고 add(a, ++b)를 실행시켰을 때는 b가 기존 대비 1만큼 올라간 값을 기준으로 add 함수를 타게 된다. (함수의 인자값으로 들어갈 때도 동일하다는 것을 보여주는 것)

 

 

 

 

 

Chapter 3.4 sizeof, 쉼표 연산자, 조건부 연산자

 

 

sizeof와 쉼표 연산자, 조건부 연산자에 대해서 설명하는 챕터이다.

 

우선 sizeof에 대해서 정리해 보자.

 

 

수업코드 1

using namespace std;

float a;

// sizeof는 데이터 타입을 넣거나, 변수를 넣을 수 있다.
// struct나 class 처럼 사용자가 만든 자료형에도 사용할 수 있음.
// sizeof는 연산자(Operator)임.
// 변수명에 사용하는 경우는 괄호를 사용하지 않더라도 가능.
cout << sizeof(float) << endl; // 4 바이트, 32비트 
cout << sizeof(a) << endl; // 4바이트, 32비트
cout << sizeof a << endl;

 

 

 

sizeof는 변수나 데이터 타입의 size를 출력하는 연산자이다. 출력 기준은 byte이다. 

 

자료형을 direct로 인자값으로 넣을 수도 있고, 혹은 변수명을 인자값으로 넣을 수도 있다. 

 

 

 

수업 코드 2

// comma operator
int x = 3;
int y = 10;
int z = (++x, ++y);
// ++x;
// ++y;
// int z = y;
cout << "x: " << x << " " << " y: " << y << " " << " z: " << z << endl;

int a1 = 1, b1 = 10;
int z1, z2;

z1 = a1, b1; // =가 , 보다 우선순위가 더 높다, 따라서 결과가 z1 = a1를 하는 것과 동일한 상황임.
z2 = (++a1, a1 + b1);

cout << z1 << endl;
cout << z2 << endl;

 

 

 

 

다음은 쉼표 연산자에 대한 설명이다.

 

z = (++x, ++y)로 작성하는 경우, 먼저 왼쪽을 실행하고(++x),  다음으로 오른쪽을 실행하고(++y), z에 y 값을 할당한다.

 

따라서 아래 출력 값을 확인해 보면, x는 4가 되어 있고, y는 11이 되어 있으며, z에는 y 값인 11이 할당되어 있는 것을 확인할 수 있다. 

 

코드상으로는 3줄을 써야 하는 것을 1줄로 묶은 느낌인데, for문에서 많이 쓴다고 한다.

 

 

그리고 z1를 보면, z1 = a1, b1; 로 코드를 작성한 것을 알 수 있는데, = 연산자가 , 보다 우선순위가 높기 때문에 b1에는 아무 일도 일어나지 않고 a1 값이 z1에 할당되는 것을 확인할 수 있다. 

 

 

수업 코드 3

int getPrice(bool onSale)
{
	if (onSale) return 10;
	else return 100;
}

// conditional operator (arithmetric if)
bool onSale = true;

// const로 사용하는 경우, 처음 변수 정의할 때 값을 줘야하니 conditional operator를 사용하기에 적합.
// 조건이 복잡하거나, 값이 복잡한 경우에는 if문으로 쪼개는 것이 좋다. 간단한 경우 사용하는 것이 적합.
const int price = (onSale == true) ? 10 : 100;
//const int price = getPrice(onSale);


cout << "Price: " << price << endl;

 

 

 

 

다음은 조건 연산자에 대한 설명이다.

 

const 변수를 사용할 때, 조건에 따라 해당 변수에 값을 줘야 하는 경우 사용한다고 한다.

 

현재 코드 상으로는 onSale 변수가 true면 price가 10이고, false면 100이 되는 것을 알 수 있다.

 

삼항 연산자를 활용하는 것이기 때문에, 조건이 가급적 간단할 때만 사용하는 것을 추천한다고 한다.

 

삼항 연산자를 사용하지 않게 되면, getPrice 함수를 따로 짜서 삼항 연산자를 if문 형식으로 풀어서 구현하는 방법도 있다.

 

 

수업 코드 4

int sample_x = 5;

// ? : 보다 <<가 우선순위가 높아서 벌어지는 현상.
// cout << (x % 2 == 0) ? "even" : "odd" << endl;
cout << ((x % 2 == 0) ? "even" : "odd") << endl;

 

마지막으로 연산자 우선순위 때문에 벌어지는 문제를 보여주는 코드이다.

 

위 코드는 아예 컴파일 자체가 안되고, 다음과 같은 오류가 나온다.

 

 

 

이는? 와 :보다 <<가 우선순위가 높기 때문에, cout << (x % 2 == 0)에서 << 연산이 끊기고, 그 뒤에 ? "even" : "odd"만 딸랑 남게 되기 때문에 문제가 발생한다고 볼 수 있다.

 

따라서, 여러 가지 연산이 복합적으로 사용될 때는 괄호를 잘 활용해서 묶어줘야만 의도된 대로 코드가 작동할 수 있다.

 

 

 

 

Chapter 3.5 관계 연산자 (Relational Operators)

 

 

해당 챕터에서는 관계 연산자에 대해서 알아본다.

 

 

수업 코드 1

if (x == y)
{
    cout << "Equal ! " << endl;
}

if (x != y)
{
    cout << "Not Equal ! " << endl;
}

if (x > y)
{
    cout << "x is bigger than y" << endl;
}

if (x < y)
{
    cout << "x is smaller than y" << endl;
}

if (x >= y)
{
    cout << "x is bigger than y or equal to y" << endl;
}

if (x <= y)
{
    cout << "x is smaller than y or equal to y" << endl;
}

 

 

관계 연산자는 크게 ==, !=, >, <, >=, <=가 있다. 부등호는 수학에서 사용하는 것과 동일해서 크게 어려울 게 없고, 코딩을 처음 해보는 분이라면 같다는 표기가 ==라는 것만 주의하면 될 것 같다. 

 

int 변수에서는 크게 문제가 없지만, float나 double처럼 실수형 변수에서가 주의할 내용들이 조금 있다.

 

 

 

수업 코드 2

double d1(100 - 99.99); // 0.01
double d2(10 - 9.99);	// 0.01

const double epsilon = 1e-10;

if (abs(d1 - d2) < epsilon)
{
    cout << "Approximatedly equal" << endl;
}
else
{
    cout << "Not Equal" << endl;
}

if (d1 == d2)
{
    cout << "Equal " << endl;
}
else
{
    cout << "Not Equal" << endl;

    cout << std::setprecision(20);

    cout << "d1: " << d1 << " d2: " << d2 << endl;

    cout << abs(d1 - d2) << endl;

    if (d1 > d2) cout << "d1 > d2" << endl;
    else cout << "d1 < d2" << endl;

}

 

 

 

double 변수 d1, d2를 정의하고, 각각 두 값이 0.01이 되도록 연산해 줬다.

 

이전에 강의에서도 언급되었듯이, 실제로 우리가 인식하는 값과 컴퓨터가 계산한 값이 다를 수 있다. 

 

따라서 d1 == d2를 확인해 보면, Not Equal이 나오는 것을 확인할 수 있다.

 

그래서 실제로 왜 다른지를 확인해 보면(setprecision을 높여서 확인), 아주 엄밀하게 따지면 아주 작은 값의 차이가 있는 것을 알 수 있다.

 

그래서 보통 소수점 변수에 대해서 계산하는 경우, 두 변수간 차이(abs(d1 - d2))를 계산해서 이 값이 특정 값보다 작은 경우는 두 변수의 값이 같다고 인정하는 경우가 많다고 한다. 어느 정도의 차이까지 인정할지는 domain에 따라 다르므로, domain knowledge를 기반으로 정한다고 한다.

 

 

 

 

Chapter 3.6 논리 연산자 (Logical operators)

 

 

해당 챕터에서는 논리 연산자(!(not), &&(and), ||(or))에 대해서 알아본다.

 

 

 

수업 코드 1

int x = 2;
int y = 2;

// short circuit evaluation
// &&로 묶여 있어서 왼쪽에 있는 식을 먼저 계산하고 오른쪽 식을 진행하게 됨.
// 이때, And 연산자는 왼쪽을 판단하고 false인 경우 오른쪽 계산을 진행하지 않고 false를 return함.
if (x == 1 && y++ == 2)
{
    // do something
}

 

 

short circuit evaluation에 대한 설명이다.

 

if 조건문에서 만약 좌측 조건이 false가 되는 경우, 우측 코드가 작동하지 않는다는 설명이다.

 

좌측 조건이 true인 경우, 우측 코드가 작동하면서 y는 3으로 값이 올라가게 된다.

 

 

 

수업 코드 2

bool a = false;
bool b = false;

// De Morgan's Law
// !(a && b); <==> !a || !b
// !(a || b); <==> !a && !b

// XOR
// false false -> false
// false true -> true
// true false -> true
// true true -> false

// XOR는 이렇게 표현한다.
cout << (a != b) << endl;

 

수학 시간에 배웠던 드 모르간 법칙에 대해서도 설명이 있었다.

 

사실 수학시간에 배운 걸 그대로 적용하면 되는 거라 어렵지는 않다. 명제 파트에서 배운 내용과 비슷하다.

 

a && b(a and b)의 !(not)을 적용하게 되면 !a || !b(not a or not b)가 된다는 내용이다.

 

그리고 XOR에 대해서도 설명이 있었는데, 이는 a != b 연산으로 구현이 가능하다고 한다.

 

연산 결과를 보면, a와 b의 값이 다르면 true이고 같으면 false이므로, a와 b가 다르다는 연산으로 구현이 가능하다.

 

 

 

수업 코드 3

bool v1 = true;
bool v2 = false;
bool v3 = false;

// Logical AND가 logical OR보다 우선순위가 높다!
// 괄호를 통해서 잘 나눠주는 것이 적합하다. 
// 다른 사람들도 실수할 수 있는 부분을 배려해서 코딩하자. 
bool r1 = v1 || v2 && v3;
bool r2 = (v1 || v2) && v3;
bool r3 = v1 || (v2 && v3);

cout << "r1: " << r1 << " " << " r2: " << r2 << "  " << endl;
cout << "r3: " << r3 << endl;

 

 

 

 

다음은 And가 Or보다 우선순위가 높다는 것을 설명하는 내용이다.

 

v1 || v2 && v3로 연산하는 경우, v1 || (v2 && v3) 연산하는 것과 결과가 같다.

 

물론 And가 Or보다 우선순위가 높다는 사실을 기억하는 것도 좋지만, 코딩을 할 때 조금 더 분명하게 괄호로 잘 나눠주는 것이 더 바람직하다고 한다. 

 

 

 

 

 

Chapter 3.7 이진수 (Binary Numbers)

 

 

 

이진수에 대해서 설명하는 챕터이다. 코드로 구현하는 챕터는 아니고, 이론적인 내용으로 진행된다.

 

 

우선 10진법을 정리해보자면, 다음과 같다.

 

11이라는 숫자는 $1$x$10^1$ + $1$x$10^0$으로 이루어진다. 

 

같은 원리로, 이진법일 때 11이라는 숫자는 $1$x$2^1$ + $1$x$2^0$로 표현된다.

 

 

 

다음으로는 2진수 덧셈이 있다.

 

우리가 익숙한 10진수 덧셈과 동일하지만, 2진수는 0과 1로만 이루어져 있어서 자릿수가 2일 때 넘어간다는 점을 기억하고 있으면 된다.

 

 

    0 1 1 0

+  0 1 1 1

----------------

              1

 

       1 

    0 1 1 0

+  0 1 1 1

----------------

          0 1

 

 

    0 1 1 0

+  0 1 1 1

----------------

    1 1 0 1

 

 

로 계산이 된다.

 

더해서 2가 되면, 다음 자릿수로 숫자가 넘어간다는 점만 기억하고 있으면 된다.

 

 

 

다음으로는 음의 정수를 표현하는 방법을 알아보자. 

 

-5를 8비트, 1byte 2진수로 표현해본다고 하면, 우선 부호를 제외하고 5를 먼저 표현해 본다.

 

0 0 0 0  0 1 0 1로 표현된다.

 

여기서 보수(Complement)를 취한다. 이는 0을 1로, 1을 0으로 바꿔주면 된다.

 

1 1 1 1  1 0 1 0로 바뀐다.

 

여기서 + 1을 해준다.

 

1 1 1 1  1 0 1 1이 된다.

 

이것이 최종적으로 -5를 표현한 것으로 이해하면 된다. 

 

2진수에서 부호(-, +)를 감안해서 표현하는 경우, 맨 앞자리가 부호를 나타낸다고 보면 된다.

 

따라서 아까 5를 나타낼 때 맨 앞자리가 0이었고, -5를 나타낼 때는 맨 앞자리가 1이다.

 

마지막에 +1을 해주는 이유는, 숫자 0 때문이다.

 

0을 4bit로 표현한다면 0 0 0 0이 되는데, 이를 보수로 표현하면 1 1 1 1이다.

 

이 상태로 사용한다면 0과 -0이 존재하는 셈인 것이다.

 

따라서 여기에 +1을 해서 0 0 0 0으로 만들어준다면, 0과 -0이 모두 0 0 0 0으로 표현되는 셈이다.

 

 

 

다음으로는 10진수를 2진수로 바꾸는 법에 대해서 알아보자.

 

148을 2진수로 바꾼다고 한다면,

 

148 / 2 = 74, 나머지 0

74 / 2 = 37, 나머지 0

37 / 2 = 18, 나머지 1

18 / 2 = 9, 나머지 0

9 / 2 = 4, 나머지 1

4 / 2 = 2, 나머지 0

2 / 2 = 1, 나머지 0

1 / 2 = 0, 나머지 1

 

여기서 나머지 부분을 아래부터 위로 쪽 이어서 적으면, 1 0 0 1 0 1 0 0이 된다.

 

 

 

하나의 2진수 정수에 대해서 이를 signed int로 볼 때와 unsigned int로 볼 때를 나눠서 생각해 보자.

 

1 0 0 1   1 1 1 0이라는 2진수 정수가 있다.

 

우선 이를 signed int로 보자면, 부호가 없으므로 단순히 변환만 해주면 된다.

 

128 + 16 + 8 + 4 + 2 이므로 158이 된다.

 

unsigned int라고 하면, 맨 앞자리가 부호가 된다. 맨 앞자리가 1이므로, 이는 음수라는 의미이며, 앞에서 언급했듯이 음수 2진수 값을 표현하기 위해서는 우선 양수로 바꾼 후, 처리해줘야 한다.

 

1 0 0 1    1 1 1 0에서 보수를 취해주면,

 

0 1 1 0    0 0 0 1이 되고, 여기서 +1을 취해주면,

 

0 1 1 0    0 0 1 0이 된다.

 

이를 계산하면 64 + 32 + 2 = 98이며, 기존에 음수였으므로 -98이다.

 

 

 

Chapter 3.8 비트단위 연산자 (Bitwise Operators)

 

 

bool 자료형을 생각해 보자. 이는 0과 1만 표현하면 되니, 사실상 1 bit만 있어도 정보를 모두 표현할 수 있다.

 

그러나 메모리 할당의 가장 기본 크기는 1 byte로, 8 bit를 사용한다. 그럼 메모리의 나머지 7 bit는 노는 것이다.

 

따라서 메모리를 아끼고, 전부 계산에 사용해서 의미 있게 사용하기 위해서 비트단위 연산자를 사용하게 된다.

 

비트단위 연산자를 사용하면 계산 속도가 빠르다.

 

 

비트단위 연산자는 총 6개로, << (left shift), >> (right shift), ~(not), &(and), |(or), ^(XOR)가 있다.

 

<<는 cout에서, >>는 cin에서 볼 수 있는데, 생긴 거만 같지 사실 cout과 cin에서 사용하는 것과 의미는 다르다.

 

cout과 cin은 라이브러리에서 강제로 연산자를 오버로딩해서 사용하고 있는 케이스라고 보면 된다.

 

 

 

#include <bitset>

unsigned int a = 0b01100; 
unsigned int b = a << 1;


cout << std::bitset<5>(a) << endl; 

cout << std::bitset<5>(b) << " " << b << endl;

 

 

 

 

a는 01100으로, 원래는 값이 12이다.

 

이를 b = a << 1로 해서, left shift를 적용해 주게 되면, b는 1 숫자를 왼쪽으로 당긴 결과인 11000으로 변하게 되고, 그 값은 24가 되면서 2배가 된다.

 

이처럼, left shift 해주는 경우는 이진수 기준으로 1을 왼쪽으로 이동시켜 주고, 숫자를 2배로 올려준다.

 

만약 left shift를 4번 해주면, 2의 4 제곱만큼 커지는 개념이다. 

 

 

right shift는 반대의 개념이다. 이진수 기준으로 오른쪽으로 이동시키게 되고, 자릿수가 반대로 이동되니 오히려 숫자를 /2로 만들어준다. 

 

 

다음으로는 bitwise and, or, xor, not 정리해 본다.

 

unsigned int a = 0b01100; 
unsigned int b = 0b10110;

cout << "bitwise and " << std::bitset<5>(a & b) << endl;

cout << "bitwise or " << std::bitset<5>(a | b) << endl;

cout << "bitwise XOR " << std::bitset<5>(a ^ b) << endl;

cout << "Not " << std::bitset<5>(~a) << endl;

 

 

 

&는 이진수 기준으로 and를 나타낸다.

 

a가 01100이고 b가 10110이니, 각 자리마다 생각해 보면, 첫자리는 a가 0이고 b가 1이니 0으로,

 

두 번째 자리는 a가 1이고 b가 0이니 0, 세 번째 짜리는 a가 1이고 b가 1이니 and로 1,

 

네 번째 자리는 a가 0이고 b가 1이니 0, 마지막 자리는 a가 0이고 b가 0이니 0이다. 따라서 a & b의 결과는 00100이다.

 

bitwise or인 |도 똑같은 원리로 생각하면 되고, bitwise XOR는 이전 강의에서도 얘기했듯이 != 관점으로 생각하면 된다.

 

즉, 두 값이 달라야 1이고, 같으면 0이 되는 것이다.

 

그리고 bitwise not인 ~a는 보수를 생각하면 된다.

 

 

 

마지막으로 수업시간에 얘기해 주신 Quiz가 있다. 이를 풀어보자.

 

1. 0110 >> 2를 10진수로 결과를 표현하면?

2. 5 | 12를 10진수로 결과를 내면?

3. 5 & 12를 10진수로 결과를 내면?

4. 5 ^ 12를 10진수로 결과를 내면?

 

답은 접은 글로 적어둡니다.

 

더보기

A1: 0001 (두 번 밀리면 자릿수를 벗어난 1은 그냥 사라짐.), 10진수로는 1

A2: 13 (5를 0101로, 12를 1100로 바꾼 뒤 계산하면 됨.)

A3: 4

A4: 9

 

 

Chapter 3.9 비트 플래그, 비트 마스크 (Bit flags, Bit masks)

 

 

이 챕터에서는 비트 플래그와 비트 마스크에 대해서 다룬다.

 

 

수업 코드 1

const unsigned char opt0 = 1 << 0;
const unsigned char opt1 = 1 << 1;
const unsigned char opt2 = 1 << 2;
const unsigned char opt3 = 1 << 3;

unsigned char items_flag = 0;

// 00000000이니까, 8개에 대해서 true/false를 만들 수 있다.
cout << "No item " << bitset<8>(items_flag) << endl;

// item0 on
items_flag |= opt0;
cout << "Item0 obtained " << bitset<8>(items_flag) << endl;

// item 3 on
items_flag |= opt3;
cout << "Item3 obtained " << bitset<8>(items_flag) << endl;

// item 3 lost
items_flag &= ~opt3;
cout << "Item3 lost " << bitset<8>(items_flag) << endl;



// Has item1 ?
if (items_flag & opt1) {cout << "has Item1  " << bitset<8>(items_flag) << endl;}
else { cout << "not have Item1  " << bitset<8>(items_flag) << endl;}

// Has item0 ?
if (items_flag & opt0) { cout << "has Item0  " << bitset<8>(items_flag) << endl; }
else { cout << "not have Item0  " << bitset<8>(items_flag) << endl; }

// obtain item 2, 3
items_flag |= (opt2 | opt3);
cout << "Obtain item 2 and item 3 " << bitset<8>(items_flag) << endl;

// item 2를 가지고 있고 item 1을 가지고 있지 않은 경우
if ((items_flag & opt2) && !(items_flag & opt1))
{
    // 상태를 바꿔줄 땐 XOR 사용
    //items_flag ^= opt2;
    //items_flag ^= opt1;
    items_flag ^= (opt2 | opt1);
}

cout << bitset<8>(items_flag) << endl;

 

 

게임에서 8개의 아이템이 있다고 할 때, 각 아이템을 가지고 있는지 아닌지를 표현한다면 bool 변수 8개가 필요하다. 

 

 

하지만 아이템의 개수가 100개 1000개로 늘어난다면, 개별 변수로 선언해가지고는 감당하기 어렵다.

 

 

따라서 이를 비트 플래그라는 방식으로 대응하는 것이다. 4 byte (32bit)짜리 변수 1개면 32 종류의 아이템의 소지 여부를 bool 변수로 나타낼 수 있는 것이다.

 

 

 

 

우선 아무것도 가지고 있지 않을 때, bitset으로 출력해보면 00000000인 것을 알 수 있다.

 

다음으로, item0 번을 가지면 첫 번째 값이 1로 바뀌어야 하며, 이는 |=를 통해서 구현할 수 있다.

 

만약 item3 번을 가지고 있다가 이를 잃는다면, opt3을 not으로 바꾼 뒤 이를 and로 연산하면 된다.

 

이를 조금 더 설명해보자면,

 

기존에 0000 1001 이였을 때, ~opt3은 1111 0111이다. 여기서 & 연산자로 계산해 보면 0000 0001이 된다.

 

&가 and이니, 4번째 자리 숫자를 제외하고 나머지를 그대로 유지하게 된다. 4번째 자리는 반대로 변경된다.

 

해당 아이템을 가지고 있는지 여부는 &를 통해서 확인할 수 있는데, 이는 확인하고자 하는 자릿수만 1이고 나머지가 0인 변수와 and 연산자이기 때문에 여부를 확인할 수 있다.

 

그리고 item 2과 item 3을 한 번에 얻는다고 하면, item 2인 char와 item 3인 char를 or로 해서 합쳐주고 다시 or 연산자를 해준다.

 

마지막으로, item 2를 가지고 있고, item 1을 가지고 있지 않다면, 전자는 &로, 후자는 &를 한 뒤 not(!)을 붙여서 조건을 완성하면 된다.

 

상태를 바꿔줄 땐 XOR를 사용하는데, 이 부분도 예시를 통해 생각해 보자.

 

item flag가 0000 1001이고, 4번째 아이템의 여부를 바꾸려고 한다면, item3이 0000 1000일 태니

 

XOR의 성질은 같으면 0, 다르면 1이다.

 

그래서 0000 0001이 된다. 

 

 

수업 코드 2

const unsigned int red_mask = 0xFF0000;
const unsigned int green_mask = 0x00FF00;
const unsigned int blue_mask = 0x0000FF;

cout << bitset<32>(red_mask) << endl;
cout << bitset<32>(green_mask) << endl;
cout << bitset<32>(blue_mask) << endl;

unsigned int pixel_color = 0xDAA520;

cout << bitset<32>(pixel_color) << endl;

unsigned char green = (pixel_color & green_mask) >> 8;
unsigned char blue = pixel_color & blue_mask;
unsigned char red = (pixel_color & red_mask) >> 16;

cout << "blue: " << bitset<8>(blue) << " " << int(blue) << endl;
cout << "green: " << bitset<8>(green) << " " << int(green) << endl;
cout << "red: " << bitset<8>(red) << " " << int(red) << endl;

 

다음은 비트 마스크이다.

 

수업시간에 왜 마스크인지는 얘기를 안 해주셨지만, int에서 일부분만 추출한다는 측면에서 나머지 값들에 마스크를 씌운다는 느낌이 아닐까 하고 생각해 본다.

 

 

 

 

pixel_color는 16진수로 구성되어 있고, 각각 red값 2개 + green값 2개 + blue값 2개로 구성된다.

 

따라서 DAA520 중 DA는 red, A5는 green, 20는 blue에 해당하는 값이다. 이를 적절하게 추출하고자 하는 것이 목표이다.

 

red_mask는 0 xFF0000로 되어 있는데, 이는 FF가 255이고, 1 byte(8 bit)를 전부 1로 채운 값이 된다. 

 

그래서 출력값의 첫 번째가 0 8개 + 1 8개 + 0 16개로 구성이 되어 있는 것이다. 

 

개념적으로 보자면 앞에서 다루었던 비트 플래그와 유사하다.

 

 

blue의 경우, 맨 오른쪽 자리를 차지하기 때문에 단순히 &를 활용하면 값을 추출할 수 있다.

 

그러나 green이나 red의 경우 오른쪽에 각각 8자리, 16자리의 수가 있기 때문에 실제 값을 제대로 추출할 수 없다.

 

따라서 이전에 배웠던 비트 연산자를 이용해서, 각각 8자리, 16자리씩 오른쪽으로 이동(>> 연산자 사용)을 해야만 실제 값을 제대로 얻을 수 있다.

 

 

 

 

 

블로그에 정리해 보니, 확실히 내용을 제대로 복습할 수 있는 것 같다.

 

 

다음 글에서는 Chapter 4를 정리해보려고 한다.

 

안녕하세요?

 

 

거의 2년만에 블로그에 다시 글을 쓰네요.

 

 

요즘 C++을 기초부터 공부중인데, 블로그에 정리해두면 추후 복습할 때도 좋을 것 같아 원래 쓰던 이 블로그에 정리하려고 합니다.

 

 

현재 수강하고 있는 강의는 https://www.inflearn.com/course/following-c-plus 이고, 열심히 들어서 상반기 안에 완강하는게 목표입니다.

 

 

다음 게시물에서 뵙겠습니다.

 

 

 

 

안녕하세요.

 

오늘은 Anomaly Detection 분야의 논문 중, 최근에 나온 논문으로 제가 관심 있게 본 논문이라서 논문 review를 남기고자 합니다.

 

대학원에 있을 때는 새로운 논문이 나오면 바로 바로바로바로 follow-up 하곤 했는데, 회사에 다니고 나니 논문을 읽는 것 외에도 워낙 할게 많으니 바로바로 따라가기는 쉽지가 않네요.

 

그래도 제가 가장 관심 있는 분야라 시간이 되면 틈틈히 새로운 논문들 review 해보도록 하려고 합니다.

 

Official 코드가 나와있지 않은 관계로, 사람들이 unofficial로 implementation 코드를 만드시는 것 같은데 어느 정도 시간이 흐르고 결과가 유사하게 나오는 시점쯤이 되면 code review도 함께 진행해 보도록 하겠습니다.

 

 

https://arxiv.org/abs/2303.14535

 

EfficientAD: Accurate Visual Anomaly Detection at Millisecond-Level Latencies

Detecting anomalies in images is an important task, especially in real-time computer vision applications. In this work, we focus on computational efficiency and propose a lightweight feature extractor that processes an image in less than a millisecond on a

arxiv.org

 

 

 

1. Abstract

 

 

본 논문에서, 저자들은 computational efficiency에 집중하여 현대 GPU에서 1ms 이내로 이미지를 처리할 수 있는 lightweight feature extractor를 제안합니다. 그리고 anomalous feature를 검출하기 위해 stduent-teacher approach를 사용합니다.

 

저자들은 정상 이미지에서 추출된 feature를 예측하는 student network를 학습시킵니다.

 

Test time에서 anomaly의 검출은 student network가 추출된 feature를 예측하는데 실패하는 것을 통해 진행됩니다.

 

저자들은 정상이 아닌 이미지에 대해서 student network가 teacher network를 따라하지 못하도록 만드는 training loss를 제안합니다. 이는 student-teacher model의 computational cost를 대폭 감소시키면서 anomalous feature를 탐지하는 능력은 향상하게 됩니다.

 

저자들은 추가적으로 물건이 잘못 배치되는 것과 같은 logical anomaly를 잡아내는 문제 또한 다루고 있습니다. 저자들은 이러한 문제를 이미지를 전역적으로(globally) 분석하는 autoencoder를 효율적으로 활용해 검출하게 됩니다.

 

 

 

 

2. Introduction

 

 

최근 classification architecture들은 latency, throughput, memory consumption, 학습 가능한 파라미터의 수와 같은 특성들에 초점을 두고 있습니다. 이는 network들이 real-world application에 적용할 수 있다는 것을 보장하게 됩니다.

 

하지만, State-of-the-art anomaly detection 방법론들은, anomaly detection 성능을 증가시키기 위해서 computational efficiency를 희생하곤 합니다. 자주 사용되는 기법으로는 앙상블, large backbone의 사용, input image의 resolution을 768x768 사이즈로 올리는 방법들이 있습니다.

 

Real-world anomaly detection application에서는 방법론의 computational requirement에 제약이 있는 경우들이 많은데, 이는 anomaly를 너무 늦게 찾게 되면 상당한 경제적 피해를 가지고 오기 때문입니다. 예를 들어서, 금속 물질이 경작 기계의 내부에 들어간다거나, 혹은 기계를 작동하는 사람의 신체가 칼날에 접근하는 경우가 발생할 수 있습니다.

 

게다가 산업 현장에서의 환경은 높은 생산률을 유지하기 위해서 엄격한 runtime limit을 가지고 있는 경우가 많습니다. 따라서, 이러한 제약을 고려하지 않게 되면 생산성 감소를 만들 수 있고, 경제적 가능성을 감소시키게 됩니다. 

 

그러므로, anomaly detection 방법론이 real-world에서 사용되려면 computational cost에 대해서 필수적으로 고려해야 합니다.

 

 

 

본 논문에서, 저자들은 anomaly detection 성능을 내면서도 inference runtime까지 동시에 고려하는, 새로운 표준이 될 방법론인 EfficientAD를 제안합니다. 

 

첫 번째로, modern GPU에서 1ms 이내로 expressive feature를 계산하기 위한 efficient network architecture를 도입합니다. 

 

두 번째로, anomalous feature를 검출하기 위해, student-teacher approach를 사용합니다.

 

저자들은 정상 이미지에 대해서 pretrained teacher network를 가지고 계산된 feature를 예측하는 student network를 학습합니다. Anomalous image에 대해서는 student network가 학습되지 않았기 때문에, 이러한 이미지에 대해서는 teacher network를 따라 하는 것에 실패하게 됩니다. 따라서, test time때 student network와 teacher network의 output 사이의 큰 distance를 기반으로 anomalous image를 검출하게 됩니다. 

 

이러한 효과를 더 증가시키기 위해서, 'Asymmetric student-teacher networks for industrial anomaly detection' 논문에서는 teacher network와 student network간 architectural asymmetry를 사용하였는데요. 저자들은 대신에, 정상 이미지가 아닌 이미지에 대해서 student network가 teacher network를 따라 하지 못하게 만드는 training loss를 도입하여 loss-induced asymmetry를 도입하였습니다. 

 

이 loss는 test time에서 computational cost에도 영향을 주지 않고, architecture design에 제약을 주지 않으며, anomalous feature를 정확히 검출하면서도 student network와 teacher network 모두에 efficient network architecture를 사용할 수 있게 만듭니다.

 

 

 

Anomalous local feature를 확인하는 것은 생산된 제품에 오염이나 얼룩과 같이, 정상 이미지와 구조적으로 다른(structurally different) anomaly를 검출하게 합니다. 하지만, 어려운 문제는 정상 이미지의 position, size, arrangement와 관련된 logical constraint에 대한 이상 여부를 찾아내는 것입니다. 이러한 문제를 해결하고자, EfficientAD는 'Beyond Dents and Scratches: Logical Constraints in Unsupervised Anomaly Detection and Localization' 논문에서 제안된 autoencoder를 사용합니다. 이 autoencoder는 test time에서 학습 이미지의 logical constraint를 학습하고, violation을 탐지하게 됩니다.

 

저자들은 autoencoder의 구조와 학습 protocol을 단순화 하면서, 어떻게 이를 student-teacher model과 효율적으로 통합할 수 있는지를 논문에서 설명합니다. 또한, autoencoder와 student-teacher model의 detection result를 합치기 전에 보정해서 anomaly detection 성능을 향상할 수 있는 방법 또한 제안합니다.

 

 

저자들의 contribution은 다음과 같이 요약해볼 수 있습니다.

 

- 이미지 한 장당 2ms 이내의 latency를 보여주면서, industrial benchmark에 대해서 anomaly detection과 localization에서 SOTA 대비 상당히 성능을 향상했습니다.

- 최근 anomaly detection 방법론들과 비교했을 때 feature extraction의 속도를 몇 배 향상할 수 있는 efficient network architecture를 제안합니다.

- Inference runtime에 영향을 미치지 않으면서 student-teacher model의 이상 탐지 성능을 상당히 향상시킬 수 있는 training loss를 도입합니다.

- autoencoder 기반으로 효율적으로 logical anomaly를 검출하고, student-teacher model의 검출 결과와 통합한 후 보정할 수 있는 방법을 제안합니다.

 

 

3. Related Work

3.1 Anomaly Detection Tasks

 

간단한 내용이라서, 해당 글에서는 생략하겠습니다. 

 

 

3.2 Anomaly Detection Methods

 

전통적인 컴퓨터 비전 알고리즘은 수십 년간 산업용 이상 탐지 문제에 성공적으로 적용되어 왔습니다. 이러한 알고리즘들은 흔히 이미지 한 장당 몇 ms 이내에 이미지를 처리해야 하는 요구사항을 충족해 왔습니다. Bergmann는 이러한 방법론들을 평가해 보았고, 사물이 똑바로 위치해있지 않은 경우에는 실패한다는 것을 확인하였습니다.

 

딥러닝 기반의 방법론들은 이러한 케이스에서도 강건하게 작동한다는 것을 보여왔습니다. 최근에 성공적인 방법론들은 pretrained 되고 frozen 된 CNN의 feature space에서 밀도 추정을 하거나 outlier를 검출합니다. 만약 feature vector가 input pixel에 매핑될 수 있다면, 이들의 outlier score를 각각의 pixel에 할당해서 2D anomaly map을 만들 수 있게 됩니다.

 

최근 방법들은 다변량 정규 분포, Gaussian Mixture Models, Normalizing Flow, kNN 알고리즘을 사용하곤 합니다. kNN은 Nearest Neighbor를 찾는 것에 있어서 runtime을 많이 잡아먹게 되는데, PatchCore는 이를 해결하고자 군집된 feature vector들의 reduced database를 만들어서 여기서만 kNN을 진행하도록 합니다. 

 

 

Bergmann은 이상 탐지를 위한 student-teacher (S-T) framework를 제안하였습니다. 여기서 teacher network는 pretrained frozen CNN입니다. 해당 연구에서는 3가지 다른 receptive field 크기를 가진 teacher network를 사용합니다. 각 teacher network는 학습 이미지에서 teacher network의 output을 따라 하는 3개의 student network를 학습시킵니다. 

 

student network는 이상이 있는 이미지를 학습하는 동안에 본 적 없기 때문에, 이들에 대해서는 teacher network의 output을 예측하는 것에 실패하기 때문에 이를 기반으로 이상 탐지가 가능합니다. Bergmann은 student network의 output의 variance와 teacher network의 output 간의 distance를 가지고 anomaly score를 계산하였습니다. 

 

S-T framework의 다양한 변형 버전들이 제안되어 왔는데요. Rudolph는 teacher network의 구조를 invertible neural network로 제한시켜서 PatchCore에 버금가는 이상 탐지 성능에 도달하는 데 성공하기도 하였습니다. 본 논문에서는 Asymmetric Student Teacher (AST)  모델과 original S-T method, 그리고 EfficientAD를 비교합니다.

 

 

autoencoder나 GAN과 같은 생성 모델 또한 이상 탐지를 위해서 범용적으로 사용되어 왔습니다. 최근 autoencoder 기반의 방법들은 정상 이미지에 대해서는 정확한 복원을, 비정상 이미지에 대해서는 부정확한 복원을 한다는 것에 의존하고 있습니다. 이는 input image에 대한 복원 결과와 비교해서 이상 이미지를 찾아낼 수 있게 만들어줍니다.

 

이러한 접근법에서 발생하는 흔한 문제는 정상 이미지에 대한 부정확한 복원으로 인해 발생하는 false-positive detection 문제입니다. (정상인데 비정상으로 탐지되는 것이라고 이해하시면 됩니다.) 이를 피하고자, Bergmann은 autoencoder가 pretrained network의 feature space에서 이미지를 복원하도록 하였습니다. 게다가 해당 논문에서는 autoencoder의 output을 예측하는 neural network를 학습시켰는데, 이 network는 normal image에 대한 systematic reconstruction error도 학습하게 됩니다. (논문에서 이 systematic reconstruction error에 대한 설명이 막 자세하진 않은데, 제가 이해하기로는 autoencoder의 output에서 나타나는 global한/거시적인 경향성을 의미하는 것 같습니다. autoencoder의 복원 결과가 전반적으로 흐릿하다면, 이것의 output을 예측하도록 학습된 모델 또한 전반적으로 흐릿한 결과를 뽑게 된다는 것입니다.)

 

 

4. Method

 

저자들은 다음 subsection들에서 EfficientAD의 각 요소들을 설명하고 있습니다. 4.1에서는 pretrained neural network로부터 나오는 feature를 어떻게 효율적으로 추출할 것인지를 다룹니다. 4.2에서는 test time에서 student-teacher model을 사용해서 어떻게 anomalous feature를 검출하는지에 대해서 다룹니다. 주된 난관은 전체적인 runtime을 낮게 가져가면서도 경쟁력 있는 이상 탐지 성능을 가지는 것이었는데요. 이를 위해 student-teacher network를 학습하기 위한 loss function을 도입하였습니다. 이는 inference 시 연산량에 영향을 주지 않으면서 이상 탐지의 성능을 향상하게 됩니다.

 

4.3에서는 autoencoder 기반의 접근법을 사용해서 어떻게 logical anomaly를 잡는지를 설명합니다. 4.4에서는 student-teacher model과 autoencoder의 탐지 결과를 어떻게 결합하고 보정하는지에 대해서 설명하게 됩니다.

 

 

4.1 Efficient Patch Descriptors

 

최근 이상 탐지 방법들은 보통 WideResNet-101와 같은 pretrained network를 사용하는데요. 저자들은 깊이가 극단적으로 줄여진 네트워크를 feature extractor로 사용합니다. 이는 단 4개의 conv layer로 구성됩니다. 구조는 아래 Figure 2를 참고하시면 됩니다.

 

 

Figure 2

 

각 output 뉴런은 33x33 pixel의 receptive field를 가지고 있기 때문에, output feature vector의 각 요소들은 33x33 patch를 설명하게 됩니다. 이렇게 명확하게 대응되기 때문에, 저자들은 이 네트워크를 patch description network (PDN)이라고 명명했습니다. PDN는 한 번의 forward pass에 가변적인 사이즈의 이미지에서도 feature vector를 뽑아낼 수 있습니다.

 

'Uninformed Students: Student-Teacher Anomaly Detection With Discriminative Latent Embeddings' 논문에서도 유사하게 몇 개의 conv layer로만 이루어진 네트워크를 사용해서 feature를 뽑았는데요. 이러한 네트워크는 downsampling과 pooling이 없다 보니 꽤 높은 연산량을 요구했습니다. 

 

해당 논문에서 사용된 네트워크의 파라미터 수가 1.6 million에서 2.7 million 정도로 비교적 낮았지만, GCAD에서 사용했던 31 million 파라미터를 가진 U-Net보다도 시간도 오래 걸리고 메모리도 더 요구하는 것을 실험적으로 확인했습니다. 이는 파라미터의 수가 특정 방법론의 latency, throughput, memory footprint에 대한 적절치 못한 metric이라는 것을 의미한다고 합니다. 

 

최근 classification architecture들은 대부분 downsampling을 초기에 적용해서 feature map 사이즈를 줄이고, 시간 및 공간 복잡도를 감소시키는 편입니다. 저자들도 유사한 방식을 PDN에 적용, NVIDIA RTX A6000 GPU 기준 256x256 사이즈 이미지에서 feature를 얻는데 800μs가 소요된다고 합니다. (800μs = 0.8ms라고 보시면 됩니다.)

 

PDN이 expressive feature를 만들게 하기 위해서, 저자들은 deep pretrained classification network를 활용해 knowledge distillation를 적용합니다. ImageNet에 있는 이미지에 대해서 pretrained network에서 뽑은 feature와 PDN의 output 간의 MSE가 최소화되도록 학습이 진행됩니다. 

 

PDN은 매우 효율적인 것 외에도, 일반 deep network를 사용하는 것과 비교했을 때 또 다른 이점이 있는데요. PDN에서 만들어진 feature vector는 각 픽셀에 대응되는 33x33 patch에만 의존한다는 것입니다. 반면에, pretrained classifier에서 나온 feature는 이미지의 다른 부분에도 long-range dependency를 가지게 되죠. 이는 Figure 3을 보면 확인할 수 있습니다.

 

Figure 3

Figure 3의 위쪽 행은 output의 중심부에 위치한 1개의 feature vector에 대해서 각 input pixel에 대한 gradient를 나타낸 것입니다. 예를 들어서 모델의 output이 28x28이라고 하면, 아마 (14, 14)에 위치한 feature vector를 얘기하는 것 같은데요. 해당 vector 값에 대한 각 input pixel의 gradient 값을 시각화한 것으로 보입니다. 

 

DenseNet이나 WideResNet을 보시면, 해당 feature vector에 대한 gradient가 input image의 정중앙 pixel 외에도 꽤나 넓은 범위에 걸쳐서 분포해 있는 것을 알 수 있습니다. 위에서 언급된 대로, 이미지의 다른 부분에도 dependency가 있다는 게 바로 이런 의미입니다. 반면에, PDN은 매우 국소 부위만 영향을 미치는 것을 알 수 있으며, 이미지의 나머지 부위들은 gradient가 아주 작은 값인 것으로 보입니다.

 

이렇게 PDN은 well-defined receptive field를 가지고 있기 때문에, 이미지의 다른 부분들로 인해서 anomaly localization에 악영향을 주는 경우가 없을 것이라고 밝히고 있습니다.

 

 

4.2 Reduced Student-Teacher

 

anomalous feature vector를 탐지하기 위해서 teacher network로는 distilled PDN을 사용하고, 동일한 architecture를 student에도 그대로 적용합니다. 이를 통해서 1ms 이내로 feature를 뽑을 수 있게 됩니다.

 

그런데, 최근 모델들이 anomaly detection performance를 증가시키고자 사용한 방법들, 예를 들어 여러 개의 teacher network와 student network를 앙상블 한다던가, student와 teacher 간의 architectural asymmetry를 활용한다거나 하는 테크닉이 적용되지 않았습니다. 따라서, 저자들은 연산량에 영향을 주지 않으면서도 성능을 높이는 training loss를 도입했습니다.

 

저자들은 일반적인 S-T framework에서 학습 이미지의 수를 증가시키면 이상 이미지에 대해서 teacher network를 모방하는 능력이 향상되는 것을 확인했다고 합니다. 이는 이상 탐지 성능을 악화시키죠. 그렇다고 해서 학습 이미지 수를 너무 많이 줄이면 정상 이미지에 대한 중요한 정보를 얻지 못하게 될 수 있죠. 그래서 저자들은 student network에 충분한 이미지를 제공해서 정상 이미지에 대해서는 teacher network를 잘 모방하도록 만들면서, 동시에 anomalous image에 대해서 generalization 하지 못하도록 만들어보고자 했다고 합니다.

 

Online Hard Example Mining과 유사하게, 저자들은 student network가 teacher network를 가장 잘 못 따라 하는 부분만 loss에 영향을 주도록 했습니다. 즉, 가장 높은 loss값을 가진 output element만을 loss에 활용한다는 것입니다.

 

이를 수식으로 표현하자면, teacher network $T$와 student network $S$가 있고 training image $I$가 있으면, 각각은 $T(I) \in R^{C \times W \times H}$, $S(I) \in R^{C \times W \times H}$를 만들어 내게 됩니다. 각 $(c, w, h)$에 대해서 squared difference는 $D_{c, w, h} = (T(I)_{c, w, h} - S(I)_{c, w, h})^2$로 계산됩니다. 

 

mining factor $p_{hard} \in [0, 1]$을 기반으로, $D$에서 $p_{hard}$ quantile을 계산합니다. 이를 $d_{hard}$로 표현하고요. 위에서 구한 $D_{c, w, h}$에서 $d_{hard}$보다 크거나 같은 요소들의 평균을 training loss인 $L_{hard}$로 사용하게 됩니다. 

 

$p_{hard}$의 의미를 생각해 보면, 이를 0으로 했을 땐 일반적인 S-T loss라고 이해할 수 있죠. 실제로 저자들의 실험에서는, 이 값을 0.999로 지정했다고 합니다. 99.9 분위수라고 하니, 매우 높은 값 일부만 사용한 것을 알 수 있습니다. 이 $p_{hard}$의 효과는 Figure 4에서 볼 수 있습니다. 

 

Figure 4

 

hard feature loss를 사용하는 효과를 위 사진에서 볼 수 있는데요. 밝기는 backprop을 위해 사용된 feature vector의 차원이 얼마나 사용되었는지를 나타내고 있습니다. 못과 관련이 없는 background에는 하얀 요소들이 없는 것을 확인할 수 있으며, 이는 backprop에 사용되는 요소들이 없다는 것을 의미하고, stduent network의 output과 teacher network의 output 간의 difference가 적다는 것을 확인할 수 있습니다. 그래서 우리가 학습해야 할 사물 쪽 요소만 학습을 진행하게 됩니다.

 

 

Inference를 진행할 때 2D anomaly map $M \in R^{W \times H}$은 $M_{w, h} = C^{-1}\Sigma_{c}D_{c, w, h}$로 계산되며, 이는 채널 기반으로 평균을 낸 것이라고 이해하시면 되겠습니다. (표기상 $C^{-1}$로 표기 되어 마치 matrix처럼 보일 수 있지만, 실제로 $C$는 차원의 수 이기 때문에 512와 같은 int입니다. 그래서 실제로는 1/512와 같이 int 값의 역수라고 이해하시면 됩니다.) 

 

추가적으로, hard feature loss를 적용하면 normal image에 대해서 false-positive detection이 발생하는 경우를 피할 수 있는 장점도 있습니다. (정상이 anomaly로 탐지되는 것을 의미합니다.)

 

hard feature loss 외에도, 저자들은 loss penalty라는 것을 추가로 적용했는데요. 이는 학습 중에 student가 normal training image에 없는 이미지에 대해서 teacher를 따라 하는 것을 방해하기 위해 적용한다고 합니다. 제가 생각하기에는 조금 독특하다고 느꼈는데, student network의 학습 도중에 teacher network의 pretraining때 사용된 이미지를 학습에 사용합니다. 즉 ImageNet에 있는 이미지 중에 랜덤 하게 뽑아서 이를 학습에 활용한다는 것이지요.

 

이를 반영한 loss를 계산하면, $L_{ST} = L_{hard} + (CWH)^{-1}\Sigma_{c} \parallel S(P)_{c}\parallel^{2}_{F}$로 계산이 됩니다. 이 penalty는 out-of-distribution image에 대해서 student network가 teacher network를 모방하지 못하게 막는 역할을 합니다. 수식이 조금 어려워 보일 순 있지만, ImageNet에서 뽑은 임의의 이미지가 $P$이고, student network가 $S$이니, $S(P)$는 이미지를 넣어서 나온 output이라고 보시면 됩니다. 이를 제곱을 해주고 channel 기준으로 sum한 다음, $CWH$로 나눠줍니다. 앞에서도 언급드렸지만, $CWH$는 matrix가 아니고 int입니다.

 

4.3 Logical Anomaly Detection

 

anomaly 중에서 logical anomaly가 발생할 가능성을 배제할 수는 없는데요. logical anomaly는 물체가 잘못 위치해 있다거나 하는 경우를 얘기합니다. 그래서 이런 경우들까지 탐지할 수 있어야 합니다. 'Beyond Dents and Scratches: Logical Constraintsin Unsupervised Anomaly Detection and Localization.' 논문에서 제안된 것처럼, 저자들은 이를 잡아내기 위한 용도로 autoencoder를 사용합니다. Figure 5에 EfficientAD의 전체 방법론을 나타내주고 있습니다.

 

Figure 5

 

Figure 5를 확인해 보면, 앞에서 언급했던 student-teacher pair가 있고, 방금 전에 얘기한 autoencoder를 활용하는 것을 볼 수 있죠. autoencoder는 teacher network의 output을 예측하도록 학습된다고 합니다. 이를 수식으로 표현하자면, 이렇게 표현할 수 있습니다. Autoencoder는 $A$, training image $I$, 이를 활용하면 $A(I) \in R^{C \times W \times H}$를 output으로 뽑게 되죠. 그리고 autoencoder를 학습하는 데 사용할 loss는 $L_{AE} = (CWH)^{-1}\Sigma_{c}\parallel T(I)_{c} - A(I)_{c} \parallel^2_{F}$로 계산할 수 있습니다. $T$는 teacher network라고 하였으니, teacher network의 output과 autoencoder의 output 간 차이를 제곱해서 이를 평균 낸 값이라고 볼 수 있죠. 일반적인 autoencoder 학습에 사용하는 loss와 동일합니다.

 

student는 PDN이라는 것을 앞에서 언급했었는데요. student는 patch 기반의 network지만, autoencoder는 이미지 전체를 encode 하고 decode 하고 있습니다. 그래서 logical anomaly가 발생한 이미지에 대해서는 autoencoder가 복원을 제대로 못합니다. 하지만, 이러한 문제가 정상 이미지에도 발생하는데요. 이는 autoencoder가 fine-grained pattern 복원을 어려워하기 때문입니다. 이는 Figure 5에서도 확인할 수 있죠.

 

 

앞에서 언급했듯이, autoencoder는 teacher network의 output을 유사하게 따라가도록 학습되어 있는데요. 보시면 autoencoder에서 나온 output은 background에 있는 패턴들은 복원이 안된 걸 볼 수 있습니다. 이 부분을 얘기한다고 이해하시면 됩니다.

 

이런 문제 때문에 teacher network의 output과 autoencoder의 output 간 차이를 그대로 anomaly map으로 활용하기에는 false-positive가 발생할 수 있다고 합니다. 따라서, 저자들은 student network의 output channel의 수를 두배로 늘리고, 이를 autoencoder의 output과 teacher network의 output를 예측하도록 학습하도록 한다고 합니다.

 

이를 수식으로 표현하자면, $S'(I) \in R^{C \times W \times H}$가 student의 추가적인 output channel이라고 했을 때, student의 추가적인 loss는 다음과 같이 정의됩니다. $L_{STAE} = (CWH)^{-1}\Sigma_{c}\parallel A(I)_{c}-S'(I)_{c}\parallel^{2}_{F}$. 결론적으로 전체 training loss는 $L_{AE}$, $L_{ST}$, $L_{STAE}$의 합이라고 합니다.

 

이렇게 학습하면 student network는 autoencoder의 정상 이미지에 대한 systematic reconstruction error(예를 들면, 전체적으로 blurry 하게 복원되는 것)은 학습하면서, 동시에 anomaly에 대해서 발생하는 reconstruction error는 학습을 못하게 됩니다. 따라서, autoencoder의 output과 student network의 output 간 차이는 anomaly map으로 활용할 수 있게 됩니다. 이는 global anomaly map으로 부르게 되고요.

 

student network의 output과 teacher network의 output간 square difference를 계산하고, 이를 channel 기준으로 평균을 내서 이를 local anomaly map으로 활용합니다. 이 두 anomaly map을 결합해서 활용하고, maximum value를 image-level anomaly score로 활용합니다.

 

4.4 Anomaly Map Normalization

 

local과 global anomaly map이 있기 때문에, 이 둘을 통합해서 사용하기 전에 유사한 scale로 변경이 필요합니다. 이는 Figure 5에 나타난 것처럼, 양쪽 anomaly map 중에서 한쪽에서만 anomaly가 검출되었을 경우 중요하게 된다고 합니다. 그 외에도, 한 map에서의 noise가 결합된 map에서 정확하게 탐지하는 것이 어렵게 만들 수 있습니다.

 

정상 이미지에서의 noise의 scale을 추정하기 위해서 validation image를 별도로 사용한다고 합니다. 이들은 학습 셋에는 없는 이미지고요. 두 가지 anomaly map 각각에 대해서, validation image 전체에 대해서 모든 pixel anomaly score를 계산합니다. 그러고 나서 각각 계산된 pixel anomaly score(local, global)에 대해서 두 개의 $p$ qunatiles를 계산하는데, $q_{a}$는 anomaly score 0에 맵핑되고, $q_{b}$는 0.1에 맵핑되는 값입니다. test time에는, 계산된 각각의 맵핑에 맞춰서 normalized 된다고 보시면 됩니다. 뒤에 experiment 부분에서 보면, $q_{a}$는 0.9 quantile, $q_{b}$는 0.995 quantile을 선택해서 사용하는 것을 알 수 있습니다.

 

 

5. Experiments

 

실험 부분은 아주 간단하게만 정리하고 넘어가려고 합니다. 크게 EfficientAD-S와 EfficientAD-M이 있다는 점만 알아두시면 되고, EfficientAD-M은 conv layer의 커널 수를 두배로 늘린 것입니다. 뒤에 보시면 S와 M 각각 레이어 구성에 대해서도 다 자세히 나와 있습니다. 실험은 MVTec AD, VisA, MVTec LOCO 데이터셋에서 진행하였고요.

 

Table 1

 

Table 1은 3개의 데이터 셋 전체에 대해서 평균 성능을 보여주고 있고, 나머지 방법론에 비해서 EfficientAD가 좋은 성능을 낸다는 점을 확인할 수 있습니다. 

 

 

Table 2

 

Table 1에 대한 세부적인 내용이 Table 2에 적혀있네요. MVTec AD에 대해서는 PatchCore, PatchCore ensemble, AST 모두 98 후반대가 나오고 있어서 이미 성능 기준으로는 saturation 된 상황인데, MVTec AD LOCO 데이터셋에 대해서 성능 차이가 심합니다. 아마 LOCO는 logical anomaly가 많아서, 기존 방법론으로는 탐지가 어려운 것이 아닐까 생각됩니다. 그래서 3개 데이터 셋에 대한 평균을 내니까 EfficientAD가 SOTA로 찍히는 모습입니다.

 

Figure 6

 

여러 가지 GPU에 대해서 Latency를 비교한 표입니다. EfficientAD가 가장 낮은 수준을 나타내고 있습니다. 

 

 

Table 4

 

Table 4는 GCAD라는 모델에서 Ablation study를 진행한 모습입니다. 각각의 요소를 반영할 때마다 성능의 변화와, Latency의 변화를 보여주고 있습니다. 이 부분을 제대로 이해하려면 GCAD라는 모델에 대해서도 자세히 알면 더욱 이해하기가 쉽겠네요. 저는 아직 읽어본 논문이 아니라서 대략 감만 잡는 정도로 읽고 넘어갔습니다.

 

 

6. Conclusion

 

강력한 이상 탐지 성능과 매우 높은 연산 효율성으로 무장한 EfficientAD를 소개하는 논문이었습니다. 이는 logical anomaly와 structural anomaly를 모두 탐지하는 새로운 표준이 될 것으로 보이고요. 

 

강력한 이상 탐지 성능과 높은 연산 효율성을 기반으로 하면 이를 실제 application 환경에서 적용하기에 좋을 것으로 보이네요. 추가로 논문에서 제안한 PDN를 활용하면, 다른 이상 탐지 방법론에서도 latency를 줄이는데 도움이 될 것이라고 합니다.

 

7. Limitations

 

Student-teacher model과 autoencoder는 각각 다른 유형의 anomaly를 탐지하도록 설계가 되어 있는데요. Autoencoder는 logical anomaly를, student-teacher network는 fine-grained structural anomaly를 탐지하는 역할입니다. 하지만, fine-grained logical anomaly는 여전히 도전적인 문제로 남아 있는데요. 예를 들면 2mm만큼 더 긴 나사 같은 게 있겠죠. 이런 것들을 잡아내려면, 전통적인 측량 방법론들을 사용해야 한다고 합니다.

 

최근 이상 탐지 방법론들과 비교했을 때 한계점으로는, kNN 기반 방법론들과는 대조적으로 autoencoder를 별도로 학습해야 합니다. 

 

 

 

 

여기까지 해서 EfficientAD 논문을 정리해 보았습니다.

 

기존 방법론들이 단순히 pretrained CNN model에서 feature extraction을 하던 것에서 knowledge distillation을 통해 더 단순한 모델을 활용해서 feature extraction이 가능하도록 만들었다는 점이 굉장히 인상적이었고요.

 

또 일반적인 결함 외에도 logical anomaly까지 잡아내기 위해서 autoencoder까지 end-to-end로 연결한 process를 만들었다는 점도 흥미로웠던 것 같습니다.

 

논문 내용을 어느 정도 이해하셨다면, 논문 뒤에 있는 Appendix A을 확인해 보시면 전체 프로세스를 pseudo-code 형식으로 정리한 내용이 있습니다. 논문에서 언급된 요소들이 실제 어떤 흐름으로 진행되는지를 확인하실 수 있습니다.

 

 

추후 또 흥미로운 이상 탐지 논문을 발견하게 되면 리뷰를 진행해 보겠습니다.

 

 

감사합니다.

 

 

안녕하세요. 오늘은 크레인 인형뽑기 게임이라는 문제를 풀어보려고 합니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/64061

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

해당 문제는 2019 카카오 개발자 겨울 인턴십 코딩테스트에 나왔던 문항입니다.

 

 

 

 

 

 

 

문제 풀이:

def solution(board, moves):
    answer = 0
    crab_lst = []
    
    for m in moves:
        for i in range(len(board)):
            if board[i][m-1] != 0:
                crab_lst.append(board[i][m-1])
                board[i][m-1] = 0 # 인형을 뽑았으니 0으로 처리
                
                # 뽑은 인형이 2개 이상일 때만 터짐 발생
                if len(crab_lst) > 1:
                    if crab_lst[-1] == crab_lst[-2]: # last 2개가 같으면
                        crab_lst.pop()
                        crab_lst.pop()
                        answer += 2
                        
                break # 한 번 뽑았다면 다음 move로 넘어가야 함
                        
    return answer

먼저 우리가 몇 번째 위치에서 인형을 집을지 저장해 둔 moves에서 for문을 진행합니다.

 

주의해야 할 점은, moves는 n번 위치를 나타내기 때문에 실제로 우리가 board 배열에서 해당 위치를 찾을 때는 -1 해줘야 한다는 점입니다. (list에서는 첫 번째가 index 기준 0이므로)

 

다음으로는 board의 길이 만큼 for문을 진행하게 되는데, 이는 moves에서 지정된 위치에서 board의 맨 위부터 내려가면서 인형이 있는 위치를 찾아가는 것이라고 생각해 주시면 됩니다.

 

그래서 if board[i][m-1] != 0이 나오게 됩니다. 즉, 0이 아닌 경우(인형이 있는 경우)에 도달하면 if문에 걸리게 되는 것이죠.

 

인형이 있으면, 이를 뽑힌 인형이 저장되는 crab_lst에 저장해 주고, 인형이 뽑혔으므로 해당 위치는 인형이 없어지게 되니 이 자리를 0으로 채워줍니다.

 

그리고 문제에서 만약 뽑힌 인형이 동일한 두개가 연달아 있으면 인형이 사라진다고 했으므로, crab_lst가 2 이상은 되어야 인형이 터지는 일이 발생합니다. 이를 반영해 if len(crab_lst) > 1 : 의 조건을 걸어줍니다.

 

 

list이므로 가장 마지막과 마지막에서 두 번째는 [-1]와 [-2]로 index 접근이 가능하며, 둘이 같으면 pop으로 빼주고 answer에 2를 더해줍니다.

 

그리고 break를 꼭 해줘야 하는데, 이는 해당 번째 위치에서 인형은 한 번만 뽑기 때문입니다. 해당 break가 없으면 moves에 해당하는 m 번째 위치에서 인형을 계속 뽑게 됩니다.

 

 

 

이번 문제는 여기까지 정리하도록 하겠습니다.

 

 

감사합니다.

안녕하세요. 오늘은 키패드 누르기라는 문제를 정리해 보도록 하겠습니다.

 

https://school.programmers.co.kr/learn/courses/30/lessons/67256

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

크게 어려운 문제는 아니나, 카카오 스러운(?) 문제라서 정리해보고자 합니다.

 

 

전체 풀이:

def distance(start, end):
    mat = [["1", "2", "3"],
          ["4", "5", "6"],
          ["7", "8", "9"],
          ["*", "0", "#"]]
    start_loc = []
    end_loc = []
    dist = 0
    for idx, r in enumerate(mat):
        if str(start) in r:
            start_loc.append(idx)
            start_loc.append(r.index(str(start)))
        
        if str(end) in r:
            end_loc.append(idx)
            end_loc.append(r.index(str(end)))

    dist = abs(end_loc[0]-start_loc[0]) + abs(end_loc[1]-start_loc[1])
    return dist
    
    

def solution(numbers, hand):
    answer = ''
    left_list = ["1", "4", "7"]
    right_list = ["3", "6", "9"]
    center_list = ["2", "5", "8", "0"]
    r_loc = "#"
    l_loc = "*"
    
    for idx, num in enumerate(numbers):
        if str(num) in left_list:
            answer += "L"
            l_loc = str(num)
        elif str(num) in right_list:
            answer += "R"
            r_loc = str(num)
        else: # center에 해당하는 경우
            dist_l = distance(l_loc, num)
            dist_r = distance(r_loc, num)
            if dist_l < dist_r:
                answer += "L"
                l_loc = str(num)
            elif dist_l > dist_r:
                answer += "R"
                r_loc = str(num)
            else:
                if hand == 'left':
                    answer += "L"
                    l_loc = str(num)
                else:
                    answer += "R"
                    r_loc = str(num)
    
    
    
    return answer

 

문제에서 정의하기로는 좌측에 위치한 1, 4, 7인 경우는 무조건 왼손으로, 3 6 9인 경우는 무조건 오른손으로 타이핑하면 된다고 했기 때문에, 이런 경우는 단순히 if문을 통해서 해결하면 됩니다.

 

이때, answer만 갱신하는 것이 아니라, 왼손과 오른손의 위치 또한 갱신해주어야 합니다.

 

2, 5, 8, 0의 경우는 오른손과 왼손 중 가까운 손을 사용해야 하기 때문입니다.

 

 

만약 왼손이나 오른손으로 확정되지 않는, 2, 5, 8, 0의 경우는 거리를 계산해야 하는데요.

 

이 부분은 별도로 distance라는 함수로 빼서 정의하였습니다.

 

def distance(start, end):
    mat = [["1", "2", "3"],
          ["4", "5", "6"],
          ["7", "8", "9"],
          ["*", "0", "#"]]
    start_loc = []
    end_loc = []
    dist = 0
    for idx, r in enumerate(mat):
        if str(start) in r:
            start_loc.append(idx)
            start_loc.append(r.index(str(start)))
        
        if str(end) in r:
            end_loc.append(idx)
            end_loc.append(r.index(str(end)))

    dist = abs(end_loc[0]-start_loc[0]) + abs(end_loc[1]-start_loc[1])
    return dist

해당 함수는 start 지점부터 end 지점까지의 거리를 계산해 주는 함수입니다.

 

먼저 키패드에 해당하는 matrix를 문자열로 정의하였습니다. *와 #이 존재하므로, 일괄적으로 문자열로 정의하는 게 낫다고 판단했습니다.

 

그리고 matrix를 row 단위로 돌면서, 해당 row에 start나 end가 있으면 해당 위치를 (x, y)의 형태로 저장하도록 했습니다.

 

마지막으로는 |x1-x0| + |y1-y0|을 계산해서 return 하도록 했습니다. 

 

어차피 한 칸을 이동하는게 1로 계산되고, 별도의 제약이 없기 때문에 이렇게 계산해도 충분하다고 판단했습니다. 

 

일반적인 distance를 계산하는 방식처럼 루트를 사용하거나 하지 않기 때문에, 1칸씩 격자로 이동하기 때문이죠.

 

 

이 distance라는 함수를 활용해서 start를 왼손 위치로 잡고, end는 우리가 쳐야 하는 숫자로 입력하면 왼손 기준의 거리를 계산할 수 있고, start를 오른손 위치로 잡고, end를 우리가 쳐야 하는 숫자로 입력하면 오른손 기준의 거리를 계산할 수 있습니다.

 

이 둘 간의 거리를 바탕으로 왼손과 오른손을 결정하면 됩니다.

 

문제에 나와 있듯이, 만약 두 거리가 같을 경우는 왼손잡이인지 오른손잡이인지만 판단해서 결정해 주면 되겠죠.

 

 

사실상 문제 자체가 어려운 케이스는 아닌데, 2, 5, 8, 0인 경우에 각각 왼손, 오른손으로 부터 해당 키패드까지의 거리를 구하는 코드만 잘 짜면 되는 문제라고 보입니다.

 

이번 문제는 여기까지 정리하도록 하겠습니다.

 

감사합니다.

 

 

 

 

 

 

안녕하세요. 오늘은 Level 1 문제인 기사단원의 무기 문제를 풀어보려고 합니다.

 

 

문제 자체가 어렵진 않지만, 소수의 개수를 구하는 문제로 소수 구하는 방법에 대해서 다뤄보고자 합니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/136798

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

풀이:

def solution(number, limit, power):
    # number : 기사단원의 수
    # limit: 공격력 제한 수치
    # power: 제한 수치를 초과한 기사가 사용할 무기
    answer = 0
    num_lst = []
    
    # 약수 개수 구하기
    for n in range(1, number + 1):
        prime_num = []
        for i in range(1, int((n**0.5) + 1)):
            if n % i == 0:
                prime_num.append(i) # 앞쪽
                if i != n // i:
                    prime_num.append(n // i) # 뒤쪽

        num_lst.append(len(prime_num)) # 약수 개수 정리
    
    # 약수 개수 모아놓고 limit 넘으면 교체
    for idx, p in enumerate(num_lst):
        if p > limit:
            num_lst[idx] = power
        
    return sum(num_lst)

약수를 구할 때, 1부터 n까지 다 따지면서 나눠지는지 여부를 확인하는 것이 소수의 원리에 맞는 방법입니다.

 

하지만, 코딩테스트에서 다루는 문제들은 대부분 시간복잡도 문제가 있어 이를 그대로 다 계산하면 시간복잡도 제한에 걸리게 됩니다.

 

위 문제도 당연히 시간복잡도 이슈에 직면하게 됩니다.

 

 

하나의 수는 보통 약수의 짝이 존재합니다.

 

예를 들어, 8의 약수를 구한다고 한다면 1, 2, 4, 8가 되는데, 1과 8 / 2와 4는 각각 짝으로 생각할 수 있습니다.

 

따라서, 약수를 구할 때 1부터 8까지 모두 따지는 것보다 짝이 있다는 개념을 활용해서 이를 조금 더 빠르게 판단할 수 있습니다.

 

 

그래서 보통 사용하는 방법은, 구하려고 하는 수의 제곱근을 계산하여 제곱근 까지만 계산해 보는 방법입니다.

 

제곱근이 직관적인 16을 예시로 들겠습니다.

 

16의 약수는 1, 2, 4, 8, 16입니다. 위에서 설명드린 바와 같이, 1과 16 / 2와 8은 짝입니다. 4는 4와 짝이지만 같은 수 이므로 한 번만 카운팅 하게 되지요.

 

이처럼 어떤 수의 약수를 생각할 때는, 어떤 수의 제곱근 까지만 생각하면 모든 약수를 구할 수 있다는 것입니다.

 

 

이를 코드로 구현한 부분이 이것입니다.

for i in range(1, int((n**0.5) + 1)):
    if n % i == 0:
        prime_num.append(i) # 앞쪽
        if i != n // i:
            prime_num.append(n // i) # 뒤쪽

우리가 구하려는 수는 n이고, n의 제곱근 까지만 반복문을 진행한다는 사실을 알 수 있습니다.

 

n이 i로 나눠지면, 이를 prime_num이라는 list에 저장합니다.

 

그리고 이 경우에, 만약 i가 n을 i로 나눈 몫과 같지 않으면, n을 i로 나눈 몫을 소수로 추가합니다.

 

 

이를 16을 예시로 들어 설명 드리겠습니다.

 

for i in range(1, 4+1) 이 될 것이고, 첫 번째로 16이 1로 나눠지는지를 검토합니다. 이때 나눠지므로, 소수로 인정합니다.

 

그리고 1이 16을 1로 나눴을 때의 값과 다른지 확인합니다. 이 때는 다르므로, 16을 1로 나눈 16도 소수로 인정합니다.

 

두 번째로 16이 2로 나눠지는지를 검토합니다. 이 때 나눠지므로, 소수로 인정합니다.

 

그리고 2가 16을 2로 나눴을 때의 값(8)과 다른지 확인, 다르므로 8도 소수로 인정합니다.

 

세 번째로 16이 3으로 나눠지는지를 검토합니다. 안되므로 넘어갑니다.

 

네 번째로 16이 4로 나눠지는지를 검토합니다. 이 때 나눠지므로, 소수로 인정합니다.

 

그리고 4가 16을 4로 나눴을 때의 값과 다른지 확인합니다. 이 때는 같기 때문에 소수에 추가하지 않습니다.

 

if i != n // i를 하는 이유를 4의 케이스를 생각해 보시면 이해가 빠를 것 같습니다.

 

 

문제에서 일단 약수의 개수를 list로 만들어주고, limit보다 큰 경우는 power라고 하는 변수 값으로 바꿔준 다음 sum 해주면 문제 풀이는 끝이 납니다.

 

사실상 시간 복잡도의 제약에 걸리지 않고 소수의 개수를 구할 수 있는지가 관건인 문제라고 보시면 됩니다.

 

 

오늘 문제는 여기까지 하겠습니다.

 

 

 

 

 

안녕하세요. 오늘은  Level 1 문제인 덧칠하기 문제를 풀어보려고 합니다.

 

 

처음에 굉장히 단순하게 풀었으나, 시간 복잡도 이슈 때문에 추가적으로 고민이 필요했던 문제였습니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/161989

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

처음에 풀었던 풀이:

def solution(n, m, section):
    answer = 0
    
    while section: # section이 남은 경우만 반복
        start_point = section[0] # 출발 지점
        
        # 출발 지점에서 m만큼 갔는데 이게 n을 넘기는 경우
        if start_point + (m-1) > n:
            start_point -= (start_point + (m-1)) - n # 넘친 만큼 빼줌
        
        # m만큼 진행하기 
        for i in range(m):
            arrival = start_point + i
            if arrival in section:
                section.remove(arrival) # 있으면 제거
        
        answer += 1    
    return answer

 

일단 section이 남은 경우에만 계속 진행하면 되기 때문에, while에서 section의 길이를 받아 1보다 클 때만 반복문이 진행되도록 하였습니다.

 

그 후, section은 오름차순 정렬 되어 있으므로, 첫 번째 요소를 start_point로 잡았습니다.

 

단, start_point에서 m만큼 갔을 때 이것이 n을 넘는 경우가 발생할 수 있습니다.

 

이런 경우 if 문으로 따로 빼서, start_point의 값을 조정해 주는 작업을 진행하였습니다.

 

예를 들어, n이 3이고 m이 3이면서 section이 [2]라면, start_point가 2로 잡힐 것이고, 여기서 3칸을 칠하게 되면 2, 3, 4의 3 칸을 칠해야 하는 상황이 되므로 n을 넘게 됩니다.

 

그래서, 넘친 만큼인 1 만큼을 빼줘서 start_point를 1로 조정하고, 여기서 3칸을 칠하게 만들면 됩니다.

 

 

다음으로는, m만큼 칠해야 하니 반복문을 통해서 진행해 주고, 도착지점을 arrival로 저장, 만약 arrival이 section에 있다면 이를 remove 해주는 방식으로 진행하였습니다.

 

하지만, 약 6개 정도의 test case에서 시간 에러가 나는 문제가 발생했습니다.

 

그래서 어떤 부분이 문제일까.. 하고 고민해 보니 arrival이 section에 있는지 판단하는 in이 문제가 될 수 있겠다고 판단했습니다.

 

왜냐하면 in의 경우 list 안의 위치를 찾아야 하는데, 만약 section이 매우 크다면 한번 작업에 많은 연산을 요구하게 됩니다.

 

문제에서 1 <= section의 길이 <= n로 되어 있는데, n이 최대 10만이 가능하니 remove 연산은 10만 개짜리 list에서 하나를 찾는 작업을 계속해서 반복해야 합니다.

 

따라서 이 부분을 개선하는 방향으로 고민하였습니다.

 

 

 

개선된 풀이:

def solution(n, m, section):
    answer = 0
    
    while section: # section이 남은 경우만 반복
        start_point = section[0] # 출발 지점
        
        # 출발 지점에서 m만큼 갔는데 이게 n을 넘기는 경우
        if start_point + (m-1) > n:
            start_point -= (start_point + (m-1)) - n # 넘친 만큼 빼줌
                
        target_section = section[:m] # section에서 최대한 지워져도 m개 까지만 가능하니까.
        
        # target_section에서 m개 범위 안에 들면 제거
        for tar in target_section:
            if start_point <= tar <= start_point + m - 1:
                section.remove(tar)

        answer += 1

    return answer

 

일단 모든 section을 다 돌지 않아야 한다는 게 이전 풀이에서의 교훈이기 때문에, 그럼 과연 section을 다 돌지 않으려면 어떻게 해야 하는가 를 고민했습니다.

 

start_point 지점 기준으로 결국 m개만 칠하게 되는 것이고, 이는 결국 section에서 m개만 검토하면 된다는 결론이 나옵니다. 왜냐면 section은 이미 오름차순 정렬이 되어 있기 때문에, m개보다 더 많은 양은 검토할 필요가 없습니다.

 

따라서 지울지 말지 검토해야 하는 section을 target_section으로 만들고, m개만 뽑아냈습니다.

 

section[:m]으로 하게 되면, 혹시라도 section이 m개 보다 작더라도 에러가 나지 않습니다. 

 

그리고 target_section에 있는 각각 요소들을 start_point 기준으로 m개 색칠하는 범위 내에 들어가 있는지를 판단해서 제거해 주는 방식으로 진행했습니다.

 

이렇게 수정하였더니, 시간 복잡도 문제가 해결되었습니다.

 

 

 

오늘 문제는 여기까지 하겠습니다.

 

 

+ Recent posts