TIL

[멋쟁이사자처럼 부트캠프 TIL회고] 52일차 : 혼자 공부하기는 즐거워(아님)

Cadi 2025. 1. 12. 02:10

 

 

코딩테스트 : 외계어 사전

PROGRAMMERS-962 행성에 불시착한 우주비행사 머쓱이는 외계행성의 언어를 공부하려고 합니다. 알파벳이 담긴 배열 spell과 외계어 사전 dic이 매개변수로 주어집니다. spell에 담긴 알파벳을 한번씩만 모두 사용한 단어가 dic에 존재한다면 1, 존재하지 않는다면 2를 return하도록 solution 함수를 완성해주세요.

using System;
using System.Reflection;

public class Solution
{
    public int solution(string[] spell, string[] dic)
    {
        int answer = 2;

        //dic에 있는 모든 원소를 검사함.
        for (int i = 0; i < dic.Length; i++)
        {
            //검사할 때마다 초기화

            int spellLength = 0;

            //하나하나 검사할 때 , spell에 있는 모든 것들을 꺼내서 검사
            for (int j = 0; j < spell.Length; j++)
            {
                if (dic[i].Contains(spell[j]))
                {
                    spellLength++;
                    //spell의 모든 원소가 들어가 있으면 1이됨
                    if (spellLength == spell.Length )
                    {
                        answer = 1; break;
                    }
                }
            }
        }


        return answer;
    }
}

 

처음에 spellLength == speel.Length -1을 습관적으로 했다가 고친 것 빼고는 큰 이슈는 없었다. 

 

다른 사람의 흥미로운 풀이

 

린큐로 정렬을 하고 구하는 방법도 있는것 같다. 하지만 지금은 '중복된 원소를 갖지 않습니다' 라는 제한 조건이 있어서 성립하는 방법인것 같기도 해서 잘 모르겠다 아직은. 

 

 

 

 

코딩테스트 : 저주의 숫자 3

3x 마을 사람들은 3을 저주의 숫자라고 생각하기 때문에 3의 배수와 숫자 3을 사용하지 않습니다. 3x 마을 사람들의 숫자는 다음과 같습니다.

10진법 3x 마을에서 쓰는 숫자 10진법 3x 마을에서 쓰는 숫자
1 1 6 8
2 2 7 10
3 4 8 11
4 5 9 14
5 7 10 16
정수 n이 매개변수로 주어질 때, n을 3x 마을에서 사용하는 숫자로 바꿔 return하도록 solution 함수를 완성해주세요.

using System;

public class Solution {
    public int solution(int n)
    {
        int answer = 0;
        int orignalnumber = n;

        for (int i = 1; i <= n; i++)
        {
            if (i % 3 == 0)
            {
                answer++;
                n++;
            }
            else if (i % 10 == 3 && i % 10 == 6 && i % 10 == 9)
            {
                answer++;
                n++;
            }
        }


        return orignalnumber + answer;
    }
}

 

이런 식으로 풀다가 답이 계속 틀렸다. 

 

내 생각은 원래 숫자를 저장해두고, i를 1부터 n까지 반복시키며 3의 배수이거나, 10으로 나눴을 때 나머지가 3,6,9라면 숫자를 추가해주고 , 그만큼 n의 범위도 늘어나니 n을 추가해 주는 방식이었다. 

 

문제를 다시 읽어보니, '3의 배수 숫자'와 '3이 들어간 수' 였다.....

using System;

public class Solution {
    public int solution(int n)
    {
        int answer = 0;
        int orignalnumber = n;

        for (int i = 1; i <= orignalnumber; i++)
        {
            if (i % 3 == 0)
            {
                answer++;
                orignalnumber++;
            }
            else if (i % 10 == 3 )
            {
                answer++;
                orignalnumber++;
            }
        }
        return orignalnumber;
    }
}

 

이러면 테스트 1인 15는 통과하지만 숫자가 30번대를 지나면 통과하지 못한다. 

혹은 130번대를 지나면 또 더해주어야 한다. 

지금은 n이 100보다 작다는 조건이 걸려 있어 이렇게 일일히 예외처리가 가능하지만, 

만일 n이 1000보다 작다는 조건만 있어도 다른 방식을 고안해야 할 것 같다. 

using System;

public class Solution {
    public int solution(int n)
    {
        int answer = 0;

        for (int i = 1; i <= n; i++)
        {
            if (i % 3 == 0)
            {
                answer++;
                n++;
            }
            else if (i % 10 == 3 )
            {
                answer++;
                n++;
            }
            else if (i / 10 == 3)
            {
                n++;
            }
            else if (i / 10 == 13)
            {
                n++;
            }
        }


        return n ;
    }
}

다른 사람의 흥미로운 풀이

 

왜 숫자로만 풀 생각을 했을까 ? 

문자열로 바꾼다면 오히려 손쉽게 클리어가 가능한 ...부분이었다.

3으로 나눠지는 것과, 3을 포함한 문자열로 두 개의 조건을 나누어서 해줬다면 훨씬 편한 방법이 있었을 것이다.

하나의 방법만으로 풀지 말고, 여러개의 자료형을 섞어서 쓰는 방식을 배웠으니 써먹어야지.

 

그리고 만일 반대라면 ? 숫자를 주고 일반 마을에서 쓰는 숫자로 바꿔보라고 한다면 ? 할 수 있을까 ?

 

 

 

코딩테스트 : 평행 

점 네 개의 좌표를 담은 이차원 배열  dots가 다음과 같이 매개변수로 주어집니다.

[[x1, y1], [x2, y2], [x3, y3], [x4, y4]]
주어진 네 개의 점을 두 개씩 이었을 때, 두 직선이 평행이 되는 경우가 있으면 1을 없으면 0을 return 하도록 solution 함수를 완성해보세요.

public int solution(int[,] dots)
{
    int answer = 0;

    // 점들을 배열로 변환
    int[] firstDot = new int[2] { dots[0, 0], dots[0, 1] };
    int[] secondDot = new int[2] { dots[1, 0], dots[1, 1] };
    int[] thirdDot = new int[2] { dots[2, 0], dots[2, 1] };
    int[] fourthDot = new int[2] { dots[3, 0], dots[3, 1] };

    // 1번, 2번 점을 선택한 경우
    int[] firstthird = CreateVector(firstDot, thirdDot);
    int[] secondfourth = CreateVector(secondDot, fourthDot);

    int[] firstfourth = CreateVector(firstDot, fourthDot);
    int[] secondthird = CreateVector(secondDot, thirdDot);

    if (AreVectorsEqual(firstthird, secondfourth) || AreVectorsEqual(firstfourth, secondthird))
    {
        answer = 1;
    }

    // 1번, 3번 점을 선택한 경우
    // 왜 secondthird를 재사용하지 않았냐 ! 벡터는 '방향'이 있는 값, 반대가 나온다, -를 붙여주어도
    // 같은 정답이 나오는지는 테스트할 예정
    int[] firstsecond = CreateVector(firstDot, secondDot);
    int[] thirdfourth = CreateVector(thirdDot, fourthDot);

    int[] thirdsecond = CreateVector(thirdDot, secondDot);

    if (AreVectorsEqual(firstsecond, thirdfourth) || AreVectorsEqual(firstfourth, thirdsecond))
    {
        answer = 1;
    }

    // 1번, 4번 점을 선택한 경우
    int[] fourthsecond = CreateVector(fourthDot, secondDot);
    int[] fourththird = CreateVector(fourthDot, thirdDot);

    if (AreVectorsEqual(firstsecond, fourththird) || AreVectorsEqual(firstthird, fourthsecond))
    {
        answer = 1;
    }

    return answer;
}

// 벡터 생성
int[] CreateVector(int[] pointA, int[] pointB)
{
    return new int[2] { pointB[0] - pointA[0], pointB[1] - pointA[1] };
}

// 벡터 비교
bool AreVectorsEqual(int[] vector1, int[] vector2)
{
    return vector1[0] == vector2[0] && vector1[1] == vector2[1];
}코드 양식입니다.

 

처음에는 이렇게 풀었다. 

결국 두 점을 이어야 하고, 선택한 두 점에서 다른 점들로 선분을 이어 벡터를 측정했을 때, 그 두 벡터가 같다면

평행이라고 말할 수 있다. 라는 점이었다. 대부분의 테스트 케이스를 통과했지만

지금의 경우는 '선분의 길이가 같은' 두 평행선만을 측정할 수 있다 .

 

그런데 왜 대부분의 테스트 케이스를 통과했는지 알아보고 싶었는데 그냥 '우연'일 확률이 높은 것 같다.

 

 

그래서 벡터의 크기를 normalize해서 다시 풀어보기로 했다.

 

벡터의 크기를 normalize하는 공식이 유니티에만 있어서 살짝쿵 도움을 받아 풀었다.

using System;

public class Solution {

public int solution(int[,] dots) 
{
    int answer = 0;

    // 점들을 배열로 변환
    int[] firstDot = new int[2] { dots[0, 0], dots[0, 1] };
    int[] secondDot = new int[2] { dots[1, 0], dots[1, 1] };
    int[] thirdDot = new int[2] { dots[2, 0], dots[2, 1] };
    int[] fourthDot = new int[2] { dots[3, 0], dots[3, 1] };

    // 1 2번 선택
    if (AreVectorsEqual(CreateNormalizedVector(firstDot, secondDot), 
            CreateNormalizedVector(thirdDot, fourthDot)) ||
        AreVectorsEqual(CreateNormalizedVector(firstDot, secondDot), 
            CreateNormalizedVector(fourthDot, thirdDot)))
    {
        answer = 1;
    }
    if (AreVectorsEqual(CreateNormalizedVector(firstDot, thirdDot), 
            CreateNormalizedVector(secondDot, fourthDot)) ||
        AreVectorsEqual(CreateNormalizedVector(firstDot, thirdDot), 
            CreateNormalizedVector(fourthDot, secondDot)))
    {
        answer = 1;
    }
    if (AreVectorsEqual(CreateNormalizedVector(firstDot, fourthDot), 
            CreateNormalizedVector(secondDot, thirdDot)) ||
        AreVectorsEqual(CreateNormalizedVector(firstDot, fourthDot), 
            CreateNormalizedVector(thirdDot, secondDot)))
    {
        answer = 1;
    }

    return answer;
}

double[] CreateNormalizedVector(int[] pointA, int[] pointB)
{
    int x = pointB[0] - pointA[0];
    int y = pointB[1] - pointA[1];
    return NormalizeVector(x, y);
}

double[] NormalizeVector(int x, int y)
{
    double magnitude = MathF.Sqrt(x * x + y * y); // 벡터의 크기 계산
    if (magnitude == 0)
    {
        return new double[] { 0, 0 }; // 크기가 0인 벡터는 방향이 없으므로 (0, 0) 반환
    }
    return new double[] { x / magnitude, y / magnitude };
}

bool AreVectorsEqual(double[] vector1, double[] vector2)
{
    return Math.Abs(vector1[0] - vector2[0]) < 1e-9 && Math.Abs(vector1[1] - vector2[1]) < 1e-9;
}
}

하나가 틀리다...... 왠진 모르겠지만 오기가 생겨서 다음에 다시 도전해야겠다. 

 

 

내일 또 해보려다가 ,기울기를 그냥 비교하면 되지 않을까 ? 라는 생각에 비교해봤다. 

근데 이번에도 또 하나가 틀리다. 원래 내 코드에서  벡터 비교 부분만 바꾼

using System;

public class Solution {
    public int solution(int[,] dots)
    {
        int answer = 0;

        // 점들을 배열로 변환
        int[] firstDot = new int[2] { dots[0, 0], dots[0, 1] };
        int[] secondDot = new int[2] { dots[1, 0], dots[1, 1] };
        int[] thirdDot = new int[2] { dots[2, 0], dots[2, 1] };
        int[] fourthDot = new int[2] { dots[3, 0], dots[3, 1] };

        // 1번, 2번 점을 선택한 경우
        int[] firstthird = CreateVector(firstDot, thirdDot);
        int[] secondfourth = CreateVector(secondDot, fourthDot);

        int[] firstfourth = CreateVector(firstDot, fourthDot);
        int[] secondthird = CreateVector(secondDot, thirdDot);

        if (VectorsEqual(firstthird, secondfourth) || VectorsEqual(firstfourth, secondthird))
        {
            answer = 1;
        }

        // 1번, 3번 점을 선택한 경우
        // 왜 secondthird를 재사용하지 않았냐 ! 벡터는 '방향'이 있는 값, 반대가 나온다, -를 붙여주어도
        // 같은 정답이 나오는지는 테스트할 예정
        int[] firstsecond = CreateVector(firstDot, secondDot);
        int[] thirdfourth = CreateVector(thirdDot, fourthDot);

        int[] thirdsecond = CreateVector(thirdDot, secondDot);

        if (VectorsEqual(firstsecond, thirdfourth) || VectorsEqual(firstfourth, thirdsecond))
        {
            answer = 1;
        }

        // 1번, 4번 점을 선택한 경우
        int[] fourthsecond = CreateVector(fourthDot, secondDot);
        int[] fourththird = CreateVector(fourthDot, thirdDot);

        if (VectorsEqual(firstsecond, fourththird) || VectorsEqual(firstthird, fourthsecond))
        {
            answer = 1;
        }

        return answer;
    }

// 벡터 생성
    int[] CreateVector(int[] pointA, int[] pointB)
    {
        return new int[2] { pointB[0] - pointA[0], pointB[1] - pointA[1] };
    }

// 벡터 비교
    bool VectorsEqual(int[] vector1, int[] vector2)
    {
        return vector1[1]/vector1[0] == vector2[1]/vector2[0];
    }
}

 

이런 부분이다.  근데 이 경우 하나의 오차가 난다. 

나눗셈은 나머지를 버리기 때문이다. 이걸 나눗셈이 아닌 곱셈으로 바꿔주어야 한다. 

bool VectorsEqual(int[] vector1, int[] vector2)
{
    //나눗셈으로 하려고 하였는데, 0. x일때 버리게 되어 잘못된 결과가 나올 수 있음
    //예시 1,3 과 3,5 모두 나누면 0이 되어버림
    return vector1[1]*vector2[0] == vector2[1]*vector1[0];
}

 

왜 나눗셈은 안되는지 이해하는데 오래걸렸다. 지금 생각해보면 당연한건데 ㅎ...

 

다른 사람의 흥미로운 풀이

 

 

와...소리가 절로 나오게 푸신 분이 계신다. 

이건 나중에 다시 봐야지.  근데 댓글에 테스트 케이스 추가시 안풀린다는 것을 봤을때 뭔가 예외처리를 해줘야 할 듯 ?

다시 풀어볼 문제에 추가 ! 

 

 

 

 


오늘의 목표

 

  1.  빈 인벤토리 슬롯 자동 확인, 꽉 차 있으면 로깅하기
  2.  아이템 데이터가 겹치면 개수가 오르면서 획득하게 하기
  3.  아이템 버리기 기능
  4.  순환 참조 ( 아이템과 아이템 프리팹 ) 해결하기

 

1. 빈 인벤토리 슬롯 자동 확인, 꽉 차 있으면 로깅하기

 

 

public InventorySlot FindingValidSlot()
{
    for (int i = 0; i < slots.Length; i++)
    {
        if (slots[i].itemData == null)
        {
            Debug.Log("i번째 슬롯이 비어있습니다");

            return slots[i];
        }
    }
    Debug.Log("모든 슬롯이 차 있습니다.");
    return null;
}

public void AddItemToInventory(ItemData itemData)
{
    if (targetInventorySlot != null)
    {
        targetInventorySlot.AddItem(itemData);
    }
}
if (Input.GetKeyDown(KeyCode.E))
{
    if (playerCursor.canPickUp)
    {
        inventory.targetInventorySlot = inventory.FindingValidSlot();
        if (inventory.targetInventorySlot != null)
        {
            Debug.Log(playerCursor.currentItem.ItemData.ItemName);
            inventory.AddItemToInventory(playerCursor.currentItem.ItemData);
            playerCursor.PickUpItem();
        }
        else
        {
            Debug.Log("아이템창이 꽉 차서 주울 수 없습니다.");
        }
       
    }
}

 

 

슬롯이 하나 있는 상태에서 테스트하면 정상 작동한다. 

 

2. 아이템 데이터가 겹치면 개수가 오르면서 획득

 

public InventorySlot FindingValidSlot(ItemData itemData)
{
    for (int i = 0; i < slots.Length; i++)
    {
        if (slots[i].itemData == itemData)
        {
            Debug.Log("같은 아이템 들어옴");
            return slots[i];
        }
    }
    for (int i = 0; i < slots.Length; i++)
    {
        if (slots[i].itemData == null)
        {
            Debug.Log("i번째 슬롯이 비어있습니다");

            return slots[i];
        }
    }
    Debug.Log("모든 슬롯이 차 있습니다.");
    return null;
}

public void AddItemToInventory(ItemData itemData)
{
    if (targetInventorySlot != null)
    {
        targetInventorySlot.AddItem(itemData);
    }
}
public void AddItem(ItemData itemData)
{
   if (this.itemData ==null)
   {
      this.itemData = itemData;
      this.ItemImage.sprite = itemData.icon;
      Debug.Log($"아이템슬롯에 아이콘 :{this.itemData.icon} ");
      count++;
      ItemCount.text = count.ToString();
   }
   else
   {
      count++;
      ItemCount.text = count.ToString();
      Debug.Log($"아이템이 {count}개 되었습니다");
   }
   
}

 

 

이렇게 만들었다. 다만 지금의 경우는 모든 슬롯의 데이터를 다 검사해야 해서 마음에 들지 않는다. 

딕셔너리를 선언해서 ContainsKey함수로 itemData가 있으면 그곳에 넣어주고, 아니면 없에주는 방식이 좋을 것 같다.

(나중에 바꾸도록 하자)

 

3. 아이템 버리기 기능

 

버튼으로 만든 이유가 선택해서 버리거나, 선택해서 슬롯의 위치를 바꾸고자 한 것이었다.

그러나 일단은 F키를 누르면 버려지게 바꿔 보았다.

public void RemoveItem(InventorySlot slot,Transform transform)
{
    if (slot != null)
    {
        if (slot.count > 1)
        {
            
            slot.count--;
            Debug.Log($"아이템 {slot.count}개 남음");
            Instantiate(slot.itemData.itemprefab, 
                transform.position + Vector3.forward, Quaternion.identity);
        }
        else
        {
            Debug.Log("아이템 0개");
            Instantiate(slot.itemData.itemprefab, 
                transform.position + Vector3.forward, Quaternion.identity);
            slot.itemData = null;
            slot.count = 0;
            slot.ItemImage.sprite = null;
        }
    }
}
if (inventory.gameObject.activeInHierarchy)
{
    if (Input.GetKeyDown(KeyCode.F))
    {
        Debug.Log("아이템 버리기 시작");
        inventory.RemoveItem(inventory.slots[0],rb.transform);
    }
}

 

슬롯을 선택하는 함수를 하나 더 만들어야 할 듯 하다. 

지금 아이템을 버리면, 그 자리에 아티메 프리팹이 하나 생성된다. 그러나 이 때 그 아이템을 다시 먹을 수 없다.

이는 4번을 해결하며 같이 해결할 것이다.

 

그리고 지금은 Rotation이 카메라 기준으로 돌아야 하지만 , 캡슐 객체 기준으로 앞으로 생성된다. 이를 해결해볼 것이다.

 

 

4. 순환 참조 문제

 

 

지금은 스크립터블오브젝트로 만든 itemData에서 itemPrefab을 참조하고 있고,

itemPrefab에서 스크립터블 오브젝트인 itemData를 참조하고 있다. 

주소만을 참조하는 것이기에 큰 문제는 되지 않지만 문제가 생길 우려가 있기 때문에 

첫 생성시 복사해서 가져오는 것으로 바꿔주었다. 

 

 

[CreateAssetMenu(fileName = "Item", menuName ="Item/ItemData")]
public class ItemData : ScriptableObject
{
   [SerializeField] private string itemName;

   public string ItemName => itemName;
   public Sprite icon;
   public GameObject itemprefab;
   
}
public class Item : MonoBehaviour
{
 [SerializeField] private ItemData itemData;

 public ItemData ItemData => itemData;

 public void Initialize(ItemData itemData)
 {
     this.itemData = itemData;
 }
 void Start()
 {
     Initialize(itemData);
 }
}

 

 

그런데 이렇게 생성하면 스크립터블 오브젝트를 쓰는 목적에 어긋나게 된다.

모든 사람이 공유하는 데이터를 '참조'형식으로 만들어 사용하고 싶어서 스크립터블 오브젝트를 만들고 읽기 전용으로 세팅한 것인데, 모든 생성된 객체마다 데이터를 복사해서 자신에게 할당하고 생성하면 의미가 없게 된다. 

 

다시 처음으로 돌아와서, 스크립터블 오브젝트의 장점을 남기면서도 순환참조의 오류를 해결하고 싶다.

우리의 목적은 스크립터블 오브젝트를 '읽기 전용으로 만들고 참조만 하는 것 '이다. 

새로운 함수를 만들고 생성할 때 아이템 데이터를 넣어주는 쪽으로 만들면 될 것 같다. 

public class Item : MonoBehaviour
{
 [SerializeField] private ItemData itemData;

 public ItemData ItemData => itemData;

 public void Initialize(ItemData itemData)
 {
     this.itemData = itemData;
 }
}

인스펙터 창에서 할당을 취소해주었다.

이러면 생성될 때 받아온 데이터를 할당하기만 한다 .

public class ItemManager : Singleton<ItemManager>
{
    public GameObject SpawnItem(ItemData itemData , Transform transform)
    {
        GameObject itemObeject = Instantiate(itemData.itemprefab, transform.position, Quaternion.identity);
        Item item = itemObeject.GetComponent<Item>();
        item.Initialize(itemData);
        return itemObeject;
    }
    
}

 

 

그런데 이렇게 작성해 버리면 씬에 새롭게 배치할 때마다 아이템 데이터를 넣어줘야 하는 단점이 있다. 

이 문제를 해결하려면 GPT 강사님은 두 가지 방법을 추천해 주셨다.

1. 이름 기반으로 아이템 자동 초기화

2. 유니티 에디터를 사용해 자동으로 초기화

 

두 가지 방법 모두 지금 당장은 사용하지 않을것 같아 일단 노션에 적어두었다. 

 

 

간단해 보이지만 이 문제만 이틀 고민했다. 안보고 넘어갔으면 큰일날뻔 ! 

유니티 에디터가 눈에 보이는 기능 외에도 인스펙터 창을 만지는 것 등 다양한 기능이 있는 것 같다 .

심심할 때 한 두 번씩 봐야지. 

 

오늘의 결과물

 

 

 

내일 목표

 

1. 원하는 슬롯의 아이템 버리기

2. 아이템을 버릴 때 '원하는 위치에' 버리기

3. 개념 정리하기(데이터, 동기/비동기)