#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
#define int ll

#define REP(i,n) for(int i=0;i<n;++i)
#define ALL(name) name.begin(),name.end()
#define SORT(name) sort(name.begin(), name.end())
#define ZERO(p) memset(p, 0, sizeof(p))
#define MINUS(p) memset(p, -1, sizeof(p))
#if 1
#  define DBG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
#  define DBG(fmt, ...)
#endif

const ll LLINF = (1LL<<60);
const int INF = (1LL<<30);
const double DINF = std::numeric_limits<double>::infinity();
const int MOD = 1000000007;
#define MAX_N 100010

typedef pair<ll, ll> hash_type;
class RollingHash {
public:
    RollingHash() {}
    RollingHash(const string &s){ Init(s); }

    // 文字列 s に対してローリングハッシュを作成します
    // O(|s|)
    void Init(const string &s) {
        int n = s.size();
        m_Hash1.assign(n + 1, 0);
        m_Hash2.assign(n + 1, 0);
        m_Pow1.assign(n + 1, 1);
        m_Pow2.assign(n + 1, 1);

        for(int i = 0;i < n; i++) {
            m_Hash1[i + 1] = (m_Hash1[i] * m_Base1 + s[i]) % m_Mod1;
            m_Hash2[i + 1] = (m_Hash2[i] * m_Base2 + s[i]) % m_Mod2;
            m_Pow1[i + 1] = (m_Pow1[i] * m_Base1) % m_Mod1;
            m_Pow2[i + 1] = (m_Pow2[i] * m_Base2) % m_Mod2;
        }
    }

    // 文字列 s の範囲 [l, r) の部分文字列に対応するローリングハッシュを返します
    // O(1)
    hash_type Get(int l, int r) const {
        ll res1 = m_Hash1[r] - m_Hash1[l] * m_Pow1[r-l] % m_Mod1;
        if(res1 < 0) { res1 += m_Mod1; }
        ll res2 = m_Hash2[r] - m_Hash2[l] * m_Pow2[r-l] % m_Mod2;
        if(res2 < 0) { res2 += m_Mod2; }
        return {res1, res2};
    }
    // 文字列全体のローリングハッシュを返します
    hash_type Get() const { return Get(0, m_Hash1.size() - 1); }

    // ハッシュ h1 で表される文字列の末尾に、
    // ハッシュ h2 , 長さ h2_len で表される文字列を連結した文字列のハッシュを返します
    // h2 のハッシュを生成した方のクラスインスタンスから呼び出してください
    // O(1)
    hash_type Concat(hash_type h1, hash_type h2, int h2_len) const {
        ll t1 = (((h1.first * m_Pow1[h2_len]) % m_Mod1) + h2.first) % m_Mod1;
        ll t2 = (((h1.second * m_Pow2[h2_len]) % m_Mod2) + h2.second) % m_Mod2;
        return {t1, t2};
    }

    // LCP (Longest Common Prefix: 最長共通接頭辞) の長さを求めます
    // 区間 [l1, r1) と、 rh インスタンスで管理される文字列の区間 [l2, r2) で
    // 共通する接頭辞の長さを求めます
    // O( log (min(r1 - l1, r2 - l2)) )
    int Lcp(const RollingHash& rh, int l1, int r1, int l2, int r2) const {
        int len = min(r1 - l1, r2 - l2);
        assert(len > 0);
        int low = -1, high = len + 1;
        while(high - low > 1) {
            int mid = (low + high) >> 1;
            if(Get(l1, l1 + mid) != rh.Get(l2, l2 + mid)) { high = mid; }
            else { low = mid; }
        }
        return low;
    }

    // 元の文字列の長さを返します
    size_t GetLength() const { return m_Hash1.size() - 1; }

private:
    const ll m_Base1 {2009};        // ベースとなる値 1
    const ll m_Base2 {1007};        // ベースとなる値 2
    const ll m_Mod1  {1000000007};  // m_Base1 で利用する剰余
    const ll m_Mod2  {1000000009};  // m_Base2 で利用する剰余
    vector<ll> m_Hash1; // m_Base1 から作られるローリングハッシュの値(ハッシュテーブル)
    vector<ll> m_Hash2; // m_Base2 から作られるローリングハッシュの値(ハッシュテーブル)
    vector<ll> m_Pow1;  // m_Base1 の i 乗 % m_Mod1
    vector<ll> m_Pow2;  // m_Base2 の i 乗 % m_Mod2
};

int N;
string S;

static inline bool Check(const RollingHash& rh, int len) {
    map<hash_type, int> exists;
    for(int i = 0; i <= (N - len); ++i) {
        hash_type ht = rh.Get(i, i + len);
        if(exists.count(ht) > 0) {
            if(exists[ht] + len <= i) { return true; }
        }
        else { exists[ht] = i; }
    }
    return false;
}

signed main() {
    cin.tie(0); cout.tie(0);
    ios::sync_with_stdio(false);

    cin >> N >> S;

    RollingHash rh(S);

    int ans = 0;

    int lb = 0, ub = N / 2 + 1;
    while(ub - lb > 1) {
        int mid = (lb + ub) >> 1;
        // 長さ mid になる場所のペア i, j が存在するか
        if(Check(rh, mid)) { lb = mid; }
        else { ub = mid; }
    }

    ans = Check(rh, ub) ? ub : lb;

    printf("%lld\n", ans);

    return 0;
}


Measured memory_bytes: 768