#include <bits/stdc++.h>
using namespace std;
//#include <boost/multiprecision/cpp_int.hpp>
//using multiInt = boost::multiprecision::cpp_int;

using ll = long long int;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
template<typename Q_temp>
using smaller_queue = priority_queue <Q_temp, vector<Q_temp>, greater<Q_temp> >;

const ll MOD = (ll) (1e9 + 7);
const int INF = (int) 1e9;
const ll LINF = (ll) 4e18;
const double PI = acos(-1.0);

#define REP(i,m,n) for(ll i = m; i < (ll)(n); ++i)
#define rep(i,n) REP(i, 0, n)
#define MP make_pair
#define MT make_tuple
#define YES(n) cout << ((n) ? "YES" : "NO") << endl
#define Yes(n) cout << ((n) ? "Yes" : "No") << endl
#define Possible(n) cout << ((n) ? "Possible" : "Impossible") << endl
#define all(v) v.begin(), v.end()
#define NP(v) next_permutation(all(v))
#define dbg(x_) cerr << #x_ << ":" << x_ << endl;
#define dbg2(x_) for(auto a_ : x_) cerr << a_ << " "; cerr << endl;
#define dbg3(x_ , sx_) rep(i, sx_) cerr << x_[i] << " "; cerr << endl;
vector<int> Dx = {0, 0, -1, 1, -1, 1, -1, 1, 0};
vector<int> Dy = {1, -1, 0, 0, -1, -1, 1, 1, 0};

inline ll CEIL(ll a, ll b) {
    return (a + b - 1) / b;
}

void finish() {
    cout << -1 << endl;
    exit(0);
}

//------------------------------------------------------

class SegmentTree {
public:
    int N; //要素数
    vector<ll> dat;
    int unit = INF; //単位元

    void init(int n_ = 200010) {
        N = 1;
        while (N < n_) N *= 2;
        dat.assign(2 * N - 1, unit);
    }

    void update(int k, int a) { //k番目(0-indexed)をaに変更
        k += N - 1; //一番下
        dat[k] = a;
        while (k > 0) {
            k = (k - 1) / 2; //1つ上がる
            //更新
            dat[k] = min(dat[k * 2 + 1], dat[k * 2 + 2]); //
        }
    }

    //[a, b)の値（最小値/和/etc.）を求める
    //kは節点の番号、l, rはその節点が[l,r)に対応づいていることを表す
    //外からはquery(a, b)と呼ぶ

    int query(int a, int b, int k = 0, int l = 0, int r = -1) {
        if (r < 0) r = N;
        if (r <= a || b <= l) return unit;
        //[a, b)が[l,r)を完全に含む
        if (a <= l && r <= b) return dat[k];
        else {
            int vl = query(a, b, k * 2 + 1, l, (l + r) / 2);
            int vr = query(a, b, k * 2 + 2, (l + r) / 2, r);
            return min(vl, vr); //
        }
    }
};

class SuffixArray {
public:
    static const int MAX_N = 5010;
    int n, k;
    string S;
    int rank_sa[MAX_N + 1];
    int tmp[MAX_N + 1];
    int sa[MAX_N];

    SuffixArray(string S_) : S(S_) {
    }


    //(rank_sa[i],rank_sa[i+k])と(rank_sa[j],rank_sa[j+k])を比較

    class compare_sa {
        int n, k, (&rank)[MAX_N + 1];
    public:

        compare_sa(int n_, int k_, int (&rank_)[MAX_N + 1]) : n(n_), k(k_), rank(rank_) {
        }

        bool operator()(int i, int j) {
            if (rank[i] != rank[j]) return rank[i] < rank[j];
            else {
                int ri = i + k <= n ? rank[i + k] : -1;
                int rj = j + k <= n ? rank[j + k] : -1;
                return ri < rj;
            }
        }
    };

    //文字列Sの接尾辞配列を構築

    void construct_sa() {
        n = S.length();

        //最初は1文字，ランクは文字コードにすればよい
        for (int i = 0; i <= n; ++i) {
            sa[i] = i;
            rank_sa[i] = i < n ? S[i] : -1;
        }

        //k文字についてソートされているところから，2k文字でソートする
        for (k = 1; k <= n; k *= 2) {

            compare_sa csa(n, k, rank_sa);
            sort(sa, sa + n + 1, csa);

            //いったんtmpに次のランクを計算し，それからrank_saに移す
            tmp[sa[0]] = 0;
            for (int i = 1; i <= n; ++i) {
                tmp[sa[i]] = tmp[sa[i - 1]] + (csa(sa[i - 1], sa[i]) ? 1 : 0);
            }
            for (int i = 0; i <= n; ++i) rank_sa[i] = tmp[i];
        }
    }


    //SにTが含まれるかどうか

    bool contain(string T) {
        int a = 0, b = S.length();
        while (b - a > 1) {
            int c = (a + b) / 2;
            //Sのsa[c]文字目から|T|文字をTと比較
            if (S.compare(sa[c], T.length(), T) < 0) a = c;
            else b = c;
        }
        return S.compare(sa[b], T.length(), T) == 0;
    }

    //----------LCP----------

    int rank_lcp[MAX_N + 1];
    int lcp[MAX_N];
    //文字列Sとその接尾辞配列saを受け取り，高さ配列をlcpに計算

    void construct_lcp() {
        int n = S.length();
        for (int i = 0; i <= n; ++i) rank_lcp[sa[i]] = i;

        int h = 0;
        lcp[0] = 0;
        for (int i = 0; i < n; ++i) {
            //文字列中での位置iの接尾辞と，接尾辞配列中でその1つ前の接尾辞のLCPを求める
            int j = sa[rank_lcp[i] - 1];

            //hを先頭の分1減らし，後ろが一致しているだけ増やす
            if (h > 0) h--;
            for (; j + h < n && i + h < n; ++h) {
                if (S[j + h] != S[i + h]) break;
            }

            lcp[rank_lcp[i] - 1] = h;
        }
    }

    SegmentTree sg;

    void construct_LCP_RMQ() {
        sg.init(n);
        for (int i = 0; i < n; ++i) {
            sg.update(i, lcp[i]);
        }
    }

    void prepare_all() {
        construct_sa();
        construct_lcp();
        construct_LCP_RMQ();
    }


    //末尾からi,j文字の接尾辞の先頭何文字が一致しているか

    int suffix_match(int i, int j) {
        int l = min(rank_lcp[i], rank_lcp[j]), r = max(rank_lcp[i], rank_lcp[j]);
        return sg.query(l, r);
    }

};

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    cout << setprecision(20) << setiosflags(ios::fixed);
    int n;
    string s;
    cin >> n >> s;
    SuffixArray sa(s);
    sa.prepare_all();

    int ans = 0;

    rep(i, n) REP(j, i + 1, n) {
        int match = min((int) (j - i), sa.suffix_match(i, j));
        ans = max(ans, match);
    }
    cout << ans << endl;
    return 0;
}


Measured memory_bytes: 640